feat(ui): support removing a clan
Also fixes: - close modal on escape key - handle class attribute in form components correctly
This commit is contained in:
@@ -24,6 +24,14 @@ export const Checkbox = (props: CheckboxProps) => {
|
||||
// we need to separate output the input otherwise it interferes with prop binding
|
||||
const [_, rootProps] = splitProps(props, ["input"]);
|
||||
|
||||
const [styleProps, otherRootProps] = splitProps(rootProps, [
|
||||
"class",
|
||||
"size",
|
||||
"orientation",
|
||||
"inverted",
|
||||
"ghost",
|
||||
]);
|
||||
|
||||
const alignment = () =>
|
||||
(props.orientation || "vertical") == "vertical" ? "start" : "center";
|
||||
|
||||
@@ -47,14 +55,21 @@ export const Checkbox = (props: CheckboxProps) => {
|
||||
|
||||
return (
|
||||
<KCheckbox.Root
|
||||
class={cx("form-field", "checkbox", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...rootProps}
|
||||
class={cx(
|
||||
styleProps.class,
|
||||
"form-field",
|
||||
"checkbox",
|
||||
styleProps.size,
|
||||
styleProps.orientation,
|
||||
{
|
||||
inverted: styleProps.inverted,
|
||||
ghost: styleProps.ghost,
|
||||
},
|
||||
)}
|
||||
{...otherRootProps}
|
||||
>
|
||||
{(state) => (
|
||||
<Orienter orientation={props.orientation} align={alignment()}>
|
||||
<Orienter orientation={styleProps.orientation} align={alignment()}>
|
||||
<Label
|
||||
labelComponent={KCheckbox.Label}
|
||||
descriptionComponent={KCheckbox.Description}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface FieldProps {
|
||||
class?: string;
|
||||
label?: string;
|
||||
labelWeight?: "bold" | "normal";
|
||||
description?: string;
|
||||
tooltip?: string;
|
||||
ghost?: boolean;
|
||||
|
||||
@@ -11,7 +11,7 @@ import styles from "./HostFileInput.module.css";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { createSignal } from "solid-js";
|
||||
import { createSignal, splitProps } from "solid-js";
|
||||
import { Tooltip } from "@kobalte/core/tooltip";
|
||||
import { Typography } from "@/src/components/Typography/Typography";
|
||||
|
||||
@@ -40,17 +40,31 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const [styleProps, otherProps] = splitProps(props, [
|
||||
"class",
|
||||
"size",
|
||||
"orientation",
|
||||
"inverted",
|
||||
"ghost",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
class={cx("form-field", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
class={cx(
|
||||
styleProps.class,
|
||||
"form-field",
|
||||
styleProps.size,
|
||||
styleProps.orientation,
|
||||
{
|
||||
inverted: styleProps.inverted,
|
||||
ghost: styleProps.ghost,
|
||||
},
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<Orienter
|
||||
orientation={props.orientation}
|
||||
align={props.orientation == "horizontal" ? "center" : "start"}
|
||||
orientation={styleProps.orientation}
|
||||
align={styleProps.orientation == "horizontal" ? "center" : "start"}
|
||||
>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
@@ -70,12 +84,12 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
{!value() && (
|
||||
<Button
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
size={styleProps.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
class={cx(
|
||||
props.orientation === "vertical"
|
||||
styleProps.orientation === "vertical"
|
||||
? styles.vertical_button
|
||||
: styles.horizontal_button,
|
||||
)}
|
||||
@@ -92,7 +106,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
hierarchy="body"
|
||||
size="xs"
|
||||
weight="medium"
|
||||
inverted={!props.inverted}
|
||||
inverted={!styleProps.inverted}
|
||||
>
|
||||
{value()}
|
||||
</Typography>
|
||||
@@ -107,7 +121,7 @@ export const HostFileInput = (props: HostFileInputProps) => {
|
||||
: styles.horizontal_button,
|
||||
)}
|
||||
hierarchy="secondary"
|
||||
size={props.size}
|
||||
size={styleProps.size}
|
||||
startIcon="Folder"
|
||||
onClick={selectFile}
|
||||
disabled={props.disabled || props.readOnly}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface LabelProps {
|
||||
descriptionComponent: DescriptionComponent;
|
||||
size?: Size;
|
||||
label?: string;
|
||||
labelWeight?: "bold" | "normal";
|
||||
description?: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
@@ -46,7 +47,7 @@ export const Label = (props: LabelProps) => {
|
||||
hierarchy="label"
|
||||
size={props.size || "default"}
|
||||
color={props.validationState == "invalid" ? "error" : "primary"}
|
||||
weight={props.readOnly ? "normal" : "bold"}
|
||||
weight={props.labelWeight || "bold"}
|
||||
inverted={props.inverted}
|
||||
>
|
||||
{props.label}
|
||||
|
||||
@@ -85,6 +85,14 @@ export const TextArea = (props: TextAreaProps) => {
|
||||
"maxRows",
|
||||
]);
|
||||
|
||||
const [styleProps, otherProps] = splitProps(props, [
|
||||
"class",
|
||||
"size",
|
||||
"orientation",
|
||||
"inverted",
|
||||
"ghost",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
ref={(el: HTMLDivElement) => {
|
||||
@@ -92,13 +100,20 @@ export const TextArea = (props: TextAreaProps) => {
|
||||
// but not in webkit, so we capture the parent ref and query for the textarea
|
||||
textareaRef = el.querySelector("textarea")! as HTMLTextAreaElement;
|
||||
}}
|
||||
class={cx("form-field", "textarea", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
class={cx(
|
||||
styleProps.class,
|
||||
"form-field",
|
||||
"textarea",
|
||||
styleProps.size,
|
||||
styleProps.orientation,
|
||||
{
|
||||
inverted: styleProps.inverted,
|
||||
ghost: styleProps.ghost,
|
||||
},
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<Orienter orientation={props.orientation} align={"start"}>
|
||||
<Orienter orientation={styleProps.orientation} align={"start"}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
|
||||
@@ -11,6 +11,7 @@ import "./TextInput.css";
|
||||
import { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { FieldProps } from "./Field";
|
||||
import { Orienter } from "./Orienter";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export type TextInputProps = FieldProps &
|
||||
TextFieldRootProps & {
|
||||
@@ -19,15 +20,30 @@ export type TextInputProps = FieldProps &
|
||||
};
|
||||
|
||||
export const TextInput = (props: TextInputProps) => {
|
||||
const [styleProps, otherProps] = splitProps(props, [
|
||||
"class",
|
||||
"size",
|
||||
"orientation",
|
||||
"inverted",
|
||||
"ghost",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
class={cx("form-field", "text", props.size, props.orientation, {
|
||||
inverted: props.inverted,
|
||||
ghost: props.ghost,
|
||||
})}
|
||||
{...props}
|
||||
class={cx(
|
||||
styleProps.class,
|
||||
"form-field",
|
||||
"text",
|
||||
styleProps.size,
|
||||
styleProps.orientation,
|
||||
{
|
||||
inverted: styleProps.inverted,
|
||||
ghost: styleProps.ghost,
|
||||
},
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<Orienter orientation={props.orientation}>
|
||||
<Orienter orientation={styleProps.orientation}>
|
||||
<Label
|
||||
labelComponent={TextField.Label}
|
||||
descriptionComponent={TextField.Description}
|
||||
@@ -37,7 +53,7 @@ export const TextInput = (props: TextInputProps) => {
|
||||
{props.icon && !props.readOnly && (
|
||||
<Icon
|
||||
icon={props.icon}
|
||||
inverted={props.inverted}
|
||||
inverted={styleProps.inverted}
|
||||
color={props.disabled ? "tertiary" : "quaternary"}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -58,7 +58,10 @@ export const Modal = (props: ModalProps) => {
|
||||
<KDialog.Portal mount={props.mount}>
|
||||
<div class={styles.backdrop} />
|
||||
<div class={styles.contentWrapper}>
|
||||
<KDialog.Content class={cx(styles.modal_content, props.class)}>
|
||||
<KDialog.Content
|
||||
class={cx(styles.modal_content, props.class)}
|
||||
onEscapeKeyDown={props.onClose}
|
||||
>
|
||||
{contentWrapper({
|
||||
children: (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
.modal {
|
||||
@apply w-screen max-w-xl h-fit flex flex-col;
|
||||
|
||||
.content {
|
||||
@apply flex flex-col gap-3 size-full;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply flex w-full items-center justify-between;
|
||||
}
|
||||
|
||||
.remove {
|
||||
@apply flex justify-between items-center px-4 py-5 gap-8;
|
||||
@apply rounded-md bg-semantic-error-2 border-[0.0625rem] border-semantic-error-4;
|
||||
|
||||
.clanInput {
|
||||
@apply grow;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Typography } from "@/src/components/Typography/Typography";
|
||||
import { Button } from "@/src/components/Button/Button";
|
||||
import { callApi } from "@/src/hooks/api";
|
||||
import { Alert } from "@/src/components/Alert/Alert";
|
||||
import { removeClanURI } from "@/src/stores/clan";
|
||||
|
||||
const schema = v.object({
|
||||
name: v.pipe(v.optional(v.string())),
|
||||
@@ -93,6 +94,14 @@ export const ClanSettingsModal = (props: ClanSettingsModalProps) => {
|
||||
return firstFormError || formStore.response.message;
|
||||
};
|
||||
|
||||
const [removeValue, setRemoveValue] = createSignal("");
|
||||
|
||||
const removeDisabled = () => removeValue() !== props.model.details.name;
|
||||
|
||||
const onRemove = () => {
|
||||
removeClanURI(props.model.uri);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
class={cx(styles.modal)}
|
||||
@@ -118,6 +127,7 @@ export const ClanSettingsModal = (props: ClanSettingsModalProps) => {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div class={cx(styles.content)}>
|
||||
<Show when={errorMessage()}>
|
||||
<Alert type="error" title="Error" description={errorMessage()} />
|
||||
</Show>
|
||||
@@ -165,6 +175,34 @@ export const ClanSettingsModal = (props: ClanSettingsModalProps) => {
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<div class={cx(styles.remove)}>
|
||||
<TextInput
|
||||
class={cx(styles.clanInput)}
|
||||
orientation="horizontal"
|
||||
onChange={setRemoveValue}
|
||||
input={{
|
||||
value: removeValue(),
|
||||
placeholder: "Enter the name of this Clan",
|
||||
onKeyDown: (event) => {
|
||||
if (event.key == "Enter" && !removeDisabled()) {
|
||||
onRemove();
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
hierarchy="primary"
|
||||
size="s"
|
||||
startIcon="Trash"
|
||||
disabled={removeDisabled()}
|
||||
onClick={onRemove}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,6 +78,7 @@ export const SectionGeneral = (props: SectionGeneralProps) => {
|
||||
size="s"
|
||||
inverted
|
||||
label="Name"
|
||||
labelWeight="normal"
|
||||
required
|
||||
readOnly={readOnly(editing, "name")}
|
||||
orientation="horizontal"
|
||||
@@ -99,6 +100,7 @@ export const SectionGeneral = (props: SectionGeneralProps) => {
|
||||
size="s"
|
||||
inverted
|
||||
label="Platform"
|
||||
labelWeight="normal"
|
||||
required
|
||||
readOnly={readOnly(editing, "machineClass")}
|
||||
orientation="horizontal"
|
||||
@@ -119,6 +121,7 @@ export const SectionGeneral = (props: SectionGeneralProps) => {
|
||||
defaultValue={field.value ?? ""}
|
||||
size="s"
|
||||
label="Description"
|
||||
labelWeight="normal"
|
||||
inverted
|
||||
readOnly={readOnly(editing, "description")}
|
||||
tooltip={tooltipText("description", fieldsSchema()!)}
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_clan_details_schema(flake: Flake) -> dict[str, FieldSchema]:
|
||||
"""
|
||||
|
||||
inventory_store = InventoryStore(flake)
|
||||
write_info = inventory_store.get_writeability_of("meta")
|
||||
write_info = inventory_store.get_writeability()
|
||||
|
||||
field_names = retrieve_typed_field_names(InventoryMeta)
|
||||
|
||||
@@ -60,6 +60,7 @@ def get_clan_details_schema(flake: Flake) -> dict[str, FieldSchema]:
|
||||
"readonly": not is_writeable_key(f"meta.{field}", write_info),
|
||||
# TODO: Provide a meaningful reason
|
||||
"reason": None,
|
||||
"readonly_members": [],
|
||||
}
|
||||
for field in field_names
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user