Files
bknd/app/src/ui/components/form/json-schema-form/ArrayField.tsx
T

135 lines
4.4 KiB
TypeScript

import { IconLibraryPlus, IconTrash } from "@tabler/icons-react";
import type { JsonSchema } from "json-schema-library";
import { memo, useMemo } from "react";
import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
import { Dropdown } from "ui/components/overlay/Dropdown";
import { useEvent } from "ui/hooks/use-event";
import { FieldComponent } from "./Field";
import { FieldWrapper } from "./FieldWrapper";
import { useDerivedFieldContext, useFormValue } from "./Form";
import { coerce, getMultiSchema, getMultiSchemaMatched, isEqual, suffixPath } from "./utils";
export const ArrayField = ({ path = "" }: { path?: string }) => {
const { setValue, pointer, required, schema, ...ctx } = useDerivedFieldContext(path);
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
// if unique items with enum
if (schema.uniqueItems && typeof schema.items === "object" && "enum" in schema.items) {
return (
<FieldWrapper name={path} schema={schema} wrapper="fieldset">
<FieldComponent
required
name={path}
schema={schema.items}
multiple
className="h-auto"
onChange={(e: any) => {
// @ts-ignore
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
setValue(ctx.path, selected);
}}
/>
</FieldWrapper>
);
}
return (
<FieldWrapper name={path} schema={schema} wrapper="fieldset">
<ArrayIterator name={path}>
{({ value }) =>
value?.map((v, index: number) => (
<ArrayItem key={index} path={path} index={index} schema={schema} />
))
}
</ArrayIterator>
<div className="flex flex-row">
<ArrayAdd path={path} schema={schema} />
</div>
</FieldWrapper>
);
};
const ArrayItem = memo(({ path, index, schema }: any) => {
const { value, ...ctx } = useDerivedFieldContext(path, (ctx) => {
return ctx.value?.[index];
});
const itemPath = suffixPath(path, index);
let subschema = schema.items;
const itemsMultiSchema = getMultiSchema(schema.items);
if (itemsMultiSchema) {
const [, , _subschema] = getMultiSchemaMatched(schema.items, value);
subschema = _subschema;
}
const handleUpdate = useEvent((pointer: string, value: any) => {
ctx.setValue(pointer, value);
});
const handleDelete = useEvent((pointer: string) => {
ctx.deleteValue(pointer);
});
const DeleteButton = useMemo(
() => <IconButton Icon={IconTrash} onClick={() => handleDelete(itemPath)} size="sm" />,
[itemPath],
);
return (
<div key={itemPath} className="flex flex-row gap-2">
<FieldComponent
name={itemPath}
schema={subschema!}
value={value}
onChange={(e) => {
handleUpdate(itemPath, coerce(e.target.value, subschema!));
}}
className="w-full"
/>
{DeleteButton}
</div>
);
}, isEqual);
const ArrayIterator = memo(
({ name, children }: any) => {
return children(useFormValue(name));
},
(prev, next) => prev.value?.length === next.value?.length,
);
const ArrayAdd = ({ schema, path }: { schema: JsonSchema; path: string }) => {
const {
setValue,
value: { currentIndex },
...ctx
} = useDerivedFieldContext(path, (ctx) => {
return { currentIndex: ctx.value?.length ?? 0 };
});
const itemsMultiSchema = getMultiSchema(schema.items);
function handleAdd(template?: any) {
const newPath = suffixPath(path, currentIndex);
setValue(newPath, template ?? ctx.lib.getTemplate(undefined, schema!.items));
}
if (itemsMultiSchema) {
return (
<Dropdown
dropdownWrapperProps={{
className: "min-w-0",
}}
items={itemsMultiSchema.map((s, i) => ({
label: s!.title ?? `Option ${i + 1}`,
onClick: () => handleAdd(ctx.lib.getTemplate(undefined, s!)),
}))}
onClickItem={console.log}
>
<Button IconLeft={IconLibraryPlus}>Add</Button>
</Dropdown>
);
}
return <Button onClick={() => handleAdd()}>Add</Button>;
};