changed media settings to new form

This commit is contained in:
dswbx
2025-02-05 11:04:37 +01:00
parent f432473ed9
commit 4b3493a6f5
18 changed files with 110 additions and 1031 deletions

View File

@@ -0,0 +1,111 @@
import type { JSONSchema } from "json-schema-to-ts";
import { type ChangeEvent, type ReactNode, createContext, useContext, useState } from "react";
import * as Formy from "ui/components/form/Formy";
import { FieldComponent, Field as FormField, type FieldProps as FormFieldProps } from "./Field";
import { FormContextOverride, useFieldContext } from "./Form";
import { getLabel, getMultiSchemaMatched } from "./utils";
export type AnyOfFieldRootProps = {
path?: string;
schema?: Exclude<JSONSchema, boolean>;
children: ReactNode;
};
export type AnyOfFieldContext = {
path: string;
schema: Exclude<JSONSchema, boolean>;
schemas?: JSONSchema[];
selectedSchema?: Exclude<JSONSchema, boolean>;
selected: number | null;
select: (index: number | null) => void;
options: string[];
selectSchema: any;
};
const AnyOfContext = createContext<AnyOfFieldContext>(undefined!);
export const useAnyOfContext = () => {
const ctx = useContext(AnyOfContext);
if (!ctx) throw new Error("useAnyOfContext: no context");
return ctx;
};
const Root = ({ path = "", schema: _schema, children }: AnyOfFieldRootProps) => {
const { setValue, pointer, lib, value, ...ctx } = useFieldContext(path);
const schema = _schema ?? ctx.schema;
if (!schema) return `AnyOfField(${path}): no schema ${pointer}`;
const [matchedIndex, schemas = []] = getMultiSchemaMatched(schema, value);
const [selected, setSelected] = useState<number | null>(matchedIndex > -1 ? matchedIndex : null);
const options = schemas.map((s, i) => s.title ?? `Option ${i + 1}`);
const selectSchema = {
enum: options
};
const selectedSchema =
selected !== null ? (schemas[selected] as Exclude<JSONSchema, boolean>) : undefined;
function select(index: number | null) {
setValue(pointer, index !== null ? lib.getTemplate(undefined, schemas[index]) : undefined);
setSelected(index);
}
return (
<AnyOfContext.Provider
value={{ selected, select, options, selectSchema, path, schema, schemas, selectedSchema }}
>
{children}
</AnyOfContext.Provider>
);
};
const Select = () => {
const { selected, select, path, schema, selectSchema } = useAnyOfContext();
function handleSelect(e: ChangeEvent<HTMLInputElement>) {
console.log("selected", e.target.value);
const i = e.target.value ? Number(e.target.value) : null;
select(i);
}
return (
<>
<Formy.Label>{getLabel(path, schema)}</Formy.Label>
<FieldComponent
schema={selectSchema as any}
onChange={handleSelect}
value={selected ?? undefined}
className="h-8 py-1"
/>
</>
);
};
const Field = ({ name, label, ...props }: Partial<FormFieldProps>) => {
const { selected, selectedSchema, path } = useAnyOfContext();
if (selected === null) return null;
return (
<FormContextOverride path={path} schema={selectedSchema} overrideData>
<FormField key={`${path}_${selected}`} name={""} label={false} {...props} />
</FormContextOverride>
);
};
export const AnyOf = {
Root,
Select,
Field,
useContext: useAnyOfContext
};
export const AnyOfField = (props: Omit<AnyOfFieldRootProps, "children">) => {
return (
<fieldset>
<AnyOf.Root {...props}>
<legend className="flex flex-row gap-2 items-center py-2">
<AnyOf.Select />
</legend>
<AnyOf.Field />
</AnyOf.Root>
</fieldset>
);
};

View File

@@ -0,0 +1,101 @@
import { IconLibraryPlus, IconTrash } from "@tabler/icons-react";
import type { JSONSchema } from "json-schema-to-ts";
import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
import * as Formy from "ui/components/form/Formy";
import { Dropdown } from "ui/components/overlay/Dropdown";
import { FieldComponent } from "./Field";
import { FieldWrapper } from "./FieldWrapper";
import { useFieldContext } from "./Form";
import { coerce, getMultiSchema, getMultiSchemaMatched } from "./utils";
export const ArrayField = ({
path = "",
schema: _schema
}: { path?: string; schema?: Exclude<JSONSchema, boolean> }) => {
const { setValue, value, pointer, required, ...ctx } = useFieldContext(path);
const schema = _schema ?? ctx.schema;
if (!schema || typeof schema === "undefined") return `ArrayField(${path}): no schema ${pointer}`;
const itemsMultiSchema = getMultiSchema(schema.items);
function handleAdd(template?: any) {
const currentIndex = value?.length ?? 0;
const newPointer = `${path}/${currentIndex}`.replace(/\/+/g, "/");
setValue(newPointer, template ?? ctx.lib.getTemplate(undefined, schema!.items));
}
function handleUpdate(pointer: string, value: any) {
setValue(pointer, value);
}
function handleDelete(pointer: string) {
return () => {
ctx.deleteValue(pointer);
};
}
const Wrapper = ({ children }) => (
<FieldWrapper pointer={path} schema={schema} wrapper="fieldset">
{children}
</FieldWrapper>
);
if (schema.uniqueItems && typeof schema.items === "object" && "enum" in schema.items) {
return (
<Wrapper>
<Formy.Select
required
options={schema.items.enum}
multiple
value={value}
className="h-auto"
onChange={(e) => {
const selected = Array.from(e.target.selectedOptions).map((o) => o.value);
console.log("selected", selected);
setValue(pointer, selected);
}}
/>
</Wrapper>
);
}
return (
<Wrapper>
{value?.map((v, index: number) => {
const pointer = `${path}/${index}`.replace(/\/+/g, "/");
const [, , subschema] = getMultiSchemaMatched(schema.items, v);
return (
<div key={pointer} className="flex flex-row gap-2">
<FieldComponent
name={pointer}
schema={subschema!}
value={v}
onChange={(e) => {
handleUpdate(pointer, coerce(e.target.value, subschema!));
}}
className="w-full"
/>
<IconButton Icon={IconTrash} onClick={handleDelete(pointer)} size="sm" />
</div>
);
})}
{itemsMultiSchema ? (
<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>
) : (
<Button onClick={() => handleAdd()}>Add</Button>
)}
</Wrapper>
);
};

View File

@@ -0,0 +1,93 @@
import type { JSONSchema } from "json-schema-to-ts";
import type { ChangeEvent, ComponentPropsWithoutRef } from "react";
import * as Formy from "ui/components/form/Formy";
import { ArrayField } from "./ArrayField";
import { FieldWrapper } from "./FieldWrapper";
import { useFieldContext } from "./Form";
import { ObjectField } from "./ObjectField";
import { coerce, isType } from "./utils";
export type FieldProps = {
name: string;
schema?: Exclude<JSONSchema, boolean>;
onChange?: (e: ChangeEvent<any>) => void;
label?: string | false;
hidden?: boolean;
};
export const Field = ({ name, schema: _schema, onChange, label: _label, hidden }: FieldProps) => {
const { pointer, value, errors, setValue, required, ...ctx } = useFieldContext(name);
const schema = _schema ?? ctx.schema;
if (!schema) return `"${name}" (${pointer}) has no schema`;
if (isType(schema.type, "object")) {
return <ObjectField path={name} schema={schema} />;
}
if (isType(schema.type, "array")) {
return <ArrayField path={name} schema={schema} />;
}
const disabled = schema.readOnly ?? "const" in schema ?? false;
//console.log("field", name, disabled, schema, ctx.schema, _schema);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
// don't remove for now, causes issues in anyOf
/*const value = coerce(e.target.value, schema as any);
setValue(pointer, value as any);*/
const value = coerce(e.target.value, schema as any, { required });
//console.log("handleChange", pointer, e.target.value, { value });
if (!value && !required) {
ctx.deleteValue(pointer);
} else {
setValue(pointer, value);
}
}
return (
<FieldWrapper
pointer={pointer}
label={_label}
required={required}
errors={errors}
schema={schema}
debug={{ value }}
hidden={hidden}
>
<FieldComponent
schema={schema}
name={pointer}
placeholder={pointer}
required={required}
disabled={disabled}
value={value}
onChange={onChange ?? handleChange}
/>
</FieldWrapper>
);
};
export const FieldComponent = ({
schema,
...props
}: { schema: JSONSchema } & ComponentPropsWithoutRef<"input">) => {
if (!schema || typeof schema === "boolean") return null;
//console.log("field", props.name, props.disabled);
if (schema.enum) {
if (!Array.isArray(schema.enum)) return null;
let options = schema.enum;
if (schema.enum.every((v) => typeof v === "string")) {
options = schema.enum.map((v, i) => ({ value: i, label: v }));
}
return <Formy.Select {...(props as any)} options={options} />;
}
if (isType(schema.type, ["number", "integer"])) {
return <Formy.Input type="number" {...props} value={props.value ?? ""} />;
}
return <Formy.Input {...props} value={props.value ?? ""} />;
};

View File

@@ -0,0 +1,89 @@
import { Popover } from "@mantine/core";
import { IconBug } from "@tabler/icons-react";
import type { JsonError } from "json-schema-library";
import type { JSONSchema } from "json-schema-to-ts";
import { Children, type ReactElement, type ReactNode, cloneElement } from "react";
import { IconButton } from "ui/components/buttons/IconButton";
import { JsonViewer } from "ui/components/code/JsonViewer";
import * as Formy from "ui/components/form/Formy";
import { getLabel } from "./utils";
export type FieldwrapperProps = {
pointer: string;
label?: string | false;
required?: boolean;
errors?: JsonError[];
schema?: Exclude<JSONSchema, boolean>;
debug?: object;
wrapper?: "group" | "fieldset";
hidden?: boolean;
children: ReactElement | ReactNode;
};
export function FieldWrapper({
pointer,
label: _label,
required,
errors = [],
schema,
debug = {},
wrapper,
hidden,
children
}: FieldwrapperProps) {
const examples = schema?.examples || [];
const examplesId = `${pointer}-examples`;
const description = schema?.description;
const label =
typeof _label !== "undefined" ? _label : schema ? getLabel(pointer, schema) : pointer;
return (
<Formy.Group
error={errors.length > 0}
as={wrapper === "fieldset" ? "fieldset" : "div"}
className={hidden ? "hidden" : "relative"}
>
<div className="absolute right-0 top-0">
<Popover>
<Popover.Target>
<IconButton Icon={IconBug} size="xs" className="opacity-30" />
</Popover.Target>
<Popover.Dropdown>
<JsonViewer
json={{ ...debug, pointer, required, schema, errors }}
expand={6}
className="p-0"
/>
</Popover.Dropdown>
</Popover>
</div>
{label && (
<Formy.Label as={wrapper === "fieldset" ? "legend" : "label"}>
{label} {required ? "*" : ""}
</Formy.Label>
)}
<div className="flex flex-row gap-2">
<div className="flex flex-1 flex-col gap-3">
{children}
{/*{Children.count(children) === 1
? cloneElement(children, {
list: examples.length > 0 ? examplesId : undefined
})
: children}
{examples.length > 0 && (
<datalist id={examplesId}>
{examples.map((e, i) => (
<option key={i} value={e as any} />
))}
</datalist>
)}*/}
</div>
</div>
{description && <Formy.Help>{description}</Formy.Help>}
{errors.length > 0 && (
<Formy.ErrorMessage>{errors.map((e) => e.message).join(", ")}</Formy.ErrorMessage>
)}
</Formy.Group>
);
}

View File

@@ -0,0 +1,207 @@
import { Draft2019, type JsonError } from "json-schema-library";
import type { TemplateOptions as LibTemplateOptions } from "json-schema-library/dist/lib/getTemplate";
import type { JsonSchema as LibJsonSchema } from "json-schema-library/dist/lib/types";
import type { JSONSchema as $JSONSchema, FromSchema } from "json-schema-to-ts";
import { get, isEqual } from "lodash-es";
import * as immutable from "object-path-immutable";
import {
type ComponentPropsWithoutRef,
type FormEvent,
type ReactNode,
createContext,
useContext,
useEffect,
useRef,
useState
} from "react";
import { Field } from "./Field";
import { isRequired, normalizePath, prefixPointer } from "./utils";
type JSONSchema = Exclude<$JSONSchema, boolean>;
export type FormProps<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
> = Omit<ComponentPropsWithoutRef<"form">, "onChange"> & {
schema: Schema;
validateOn?: "change" | "submit";
initialValues?: Partial<Data>;
initialOpts?: LibTemplateOptions;
onChange?: (data: Partial<Data>, name: string, value: any) => void;
hiddenSubmit?: boolean;
};
export type FormContext<Data> = {
data: Data;
setData: (data: Data) => void;
setValue: (pointer: string, value: any) => void;
deleteValue: (pointer: string) => void;
errors: JsonError[];
dirty: boolean;
schema: JSONSchema;
lib: Draft2019;
};
const FormContext = createContext<FormContext<any>>(undefined!);
export function Form<
Schema extends JSONSchema = JSONSchema,
Data = Schema extends JSONSchema ? FromSchema<JSONSchema> : any
>({
schema,
initialValues: _initialValues,
initialOpts,
children,
onChange,
validateOn = "submit",
hiddenSubmit = true,
...props
}: FormProps<Schema, Data>) {
const lib = new Draft2019(schema);
const initialValues = _initialValues ?? lib.getTemplate(undefined, schema, initialOpts);
const [data, setData] = useState<Partial<Data>>(initialValues);
const [dirty, setDirty] = useState<boolean>(false);
const formRef = useRef<HTMLFormElement | null>(null);
const [errors, setErrors] = useState<JsonError[]>([]);
async function handleChange(e: FormEvent<HTMLFormElement>) {}
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
return false;
}
function setValue(pointer: string, value: any) {
const normalized = normalizePath(pointer);
//console.log("setValue", { pointer, normalized, value });
const key = normalized.substring(2).replace(/\//g, ".");
setData((prev) => {
const changed = immutable.set(prev, key, value);
onChange?.(changed, key, value);
//console.log("changed", prev, changed, { key, value });
return changed;
});
}
function deleteValue(pointer: string) {
const normalized = normalizePath(pointer);
const key = normalized.substring(2).replace(/\//g, ".");
setData((prev) => {
const changed = immutable.del(prev, key);
onChange?.(changed, key, undefined);
//console.log("changed", prev, changed, { key });
return changed;
});
}
useEffect(() => {
setDirty(!isEqual(initialValues, data));
if (validateOn === "change") {
validate();
}
}, [data]);
function validate(_data?: Partial<Data>) {
const actual = _data ?? data;
const errors = lib.validate(actual, schema);
//console.log("errors", errors);
setErrors(errors);
return { data: actual, errors };
}
const context = {
data: data ?? {},
dirty,
setData,
setValue,
deleteValue,
errors,
schema,
lib
} as any;
//console.log("context", context);
return (
<>
<form {...props} ref={formRef} onChange={handleChange} onSubmit={handleSubmit}>
<FormContext.Provider value={context}>
{children ? children : <Field name="" />}
</FormContext.Provider>
{hiddenSubmit && (
<button style={{ visibility: "hidden" }} type="submit">
Submit
</button>
)}
</form>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(errors, null, 2)}</pre>
</>
);
}
export function useFormContext() {
return useContext(FormContext);
}
export function FormContextOverride({
children,
overrideData,
path,
...overrides
}: Partial<FormContext<any>> & { children: ReactNode; path?: string; overrideData?: boolean }) {
const ctx = useFormContext();
const additional: Partial<FormContext<any>> = {};
// this makes a local schema down the three
// especially useful for AnyOf, since it doesn't need to fully validate (e.g. pattern)
if (overrideData && path) {
const pointer = normalizePath(path);
const value =
pointer === "#/" ? ctx.data : get(ctx.data, pointer.substring(2).replace(/\//g, "."));
additional.data = value;
additional.setValue = (pointer: string, value: any) => {
ctx.setValue(prefixPointer(pointer, path), value);
};
additional.deleteValue = (pointer: string) => {
ctx.deleteValue(prefixPointer(pointer, path));
};
}
const context = {
...ctx,
...overrides,
...additional
};
return <FormContext.Provider value={context}>{children}</FormContext.Provider>;
}
export function useFieldContext(name: string) {
const { data, lib, schema, errors: formErrors, ...rest } = useFormContext();
const pointer = normalizePath(name);
const isRootPointer = pointer === "#/";
//console.log("pointer", pointer);
const value = isRootPointer ? data : get(data, pointer.substring(2).replace(/\//g, "."));
const errors = formErrors.filter((error) => error.data.pointer.startsWith(pointer));
const fieldSchema = isRootPointer
? (schema as LibJsonSchema)
: lib.getSchema({ pointer, data, schema });
const required = isRequired(pointer, schema, data);
return {
...rest,
lib,
value,
errors,
schema: fieldSchema,
pointer,
required
};
}
export function Subscribe({ children }: { children: (ctx: FormContext<any>) => ReactNode }) {
const ctx = useFormContext();
return children(ctx);
}

View File

@@ -0,0 +1,46 @@
import type { JSONSchema } from "json-schema-to-ts";
import { AnyOfField } from "./AnyOfField";
import { Field } from "./Field";
import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper";
import { useFieldContext } from "./Form";
export type ObjectFieldProps = {
path?: string;
schema?: Exclude<JSONSchema, boolean>;
label?: string | false;
wrapperProps?: Partial<FieldwrapperProps>;
};
export const ObjectField = ({
path = "",
schema: _schema,
label: _label,
wrapperProps = {}
}: ObjectFieldProps) => {
const { errors, ...ctx } = useFieldContext(path);
const schema = _schema ?? ctx.schema;
if (!schema) return "ObjectField: no schema";
const properties = schema.properties ?? {};
return (
<FieldWrapper
pointer={path}
errors={errors}
schema={schema}
wrapper="fieldset"
{...wrapperProps}
>
{Object.keys(properties).map((prop) => {
const schema = properties[prop];
const pointer = `${path}/${prop}`.replace(/\/+/g, "/");
if (!schema) return;
if (schema.anyOf || schema.oneOf) {
return <AnyOfField key={pointer} path={pointer} />;
}
return <Field key={pointer} name={pointer} />;
})}
</FieldWrapper>
);
};

View File

@@ -1,98 +0,0 @@
import { Switch } from "@mantine/core";
import { autoFormatString } from "core/utils";
import { type JSONSchema, useFieldContext, useFormContext } from "json-schema-form-react";
import type { ComponentPropsWithoutRef } from "react";
import * as Formy from "ui/components/form/Formy";
// make a local version of JSONSchema that is always an object
export type FieldProps = JSONSchema & {
name: string;
defaultValue?: any;
hidden?: boolean;
overrides?: ComponentPropsWithoutRef<"input">;
};
export function Field(p: FieldProps) {
const { schema, defaultValue, required } = useFieldContext(p.name);
const props = {
...(typeof schema === "object" ? schema : {}),
defaultValue,
required,
...p
} as FieldProps;
console.log("schema", p.name, schema, defaultValue);
const field = renderField(props);
const label = props.title
? props.title
: autoFormatString(
props.name?.includes(".") ? (props.name.split(".").pop() as string) : props.name
);
return p.hidden ? (
field
) : (
<Formy.Group>
<Formy.Label>
{label}
{props.required ? " *" : ""}
</Formy.Label>
{field}
{props.description ? <Formy.Help>{props.description}</Formy.Help> : null}
</Formy.Group>
);
}
function isType(_type: JSONSchema["type"], _compare: JSONSchema["type"]) {
if (!_type || !_compare) return false;
const type = Array.isArray(_type) ? _type : [_type];
const compare = Array.isArray(_compare) ? _compare : [_compare];
return compare.some((t) => type.includes(t));
}
function renderField(props: FieldProps) {
//console.log("renderField", props.name, props);
const common = {
name: props.name,
defaultValue: typeof props.defaultValue !== "undefined" ? props.defaultValue : props.default
} as any;
if (props.hidden) {
common.type = "hidden";
}
if (isType(props.type, "boolean")) {
return (
<div className="flex flex-row">
<Switch
disabled={props.disabled}
id={props.id}
defaultChecked={props.defaultValue}
name={props.name}
/>
</div>
);
} else if (isType(props.type, ["number", "integer"])) {
return <Formy.Input type="number" {...common} />;
}
return <Formy.Input type="text" {...common} />;
}
export function AutoForm({ schema, prefix = "" }: { schema: JSONSchema; prefix?: string }) {
const required = schema.required ?? [];
const properties = schema.properties ?? {};
return (
<>
{/*<pre>{JSON.stringify(schema, null, 2)}</pre>;*/}
<div>
{Object.keys(properties).map((name) => {
const field = properties[name];
const _name = `${prefix ? prefix + "." : ""}${name}`;
return <Field key={_name} name={_name} {...(field as any)} />;
})}
</div>
</>
);
}

View File

@@ -1,2 +1,6 @@
export { TypeboxValidator, type ValueError } from "./validators/tb-validator";
export { CfValidator, type OutputUnit } from "./validators/cf-validator";
export * from "./Field";
export * from "./Form";
export * from "./ObjectField";
export * from "./ArrayField";
export * from "./AnyOfField";
export * from "./FieldWrapper";

View File

@@ -0,0 +1,202 @@
import { autoFormatString } from "core/utils";
import { Draft2019, type JsonSchema } from "json-schema-library";
import type { JSONSchema } from "json-schema-to-ts";
import type { JSONSchemaType } from "json-schema-to-ts/lib/types/definitions/jsonSchema";
import { set } from "lodash-es";
import type { FormEvent } from "react";
export function getFormTarget(e: FormEvent<HTMLFormElement>) {
const form = e.currentTarget;
const target = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null;
// check if target has attribute "data-ignore" set
// also check if target is within a "data-ignore" element
if (
!target ||
!form.contains(target) ||
!target.name ||
target.hasAttribute("data-ignore") ||
target.closest("[data-ignore]")
) {
return; // Ignore events from outside the form
}
return target;
}
export function flatten(obj: any, parentKey = "", result: any = {}): any {
for (const key in obj) {
if (key in obj) {
const newKey = parentKey ? `${parentKey}/${key}` : "#/" + key;
if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
flatten(obj[key], newKey, result);
} else if (Array.isArray(obj[key])) {
obj[key].forEach((item, index) => {
const arrayKey = `${newKey}.${index}`;
if (typeof item === "object" && item !== null) {
flatten(item, arrayKey, result);
} else {
result[arrayKey] = item;
}
});
} else {
result[newKey] = obj[key];
}
}
}
return result;
}
// @todo: make sure it's in the right order
export function unflatten(
obj: Record<string, string>,
schema: JSONSchema,
selections?: Record<string, number | undefined>
) {
const result = {};
const lib = new Draft2019(schema as any);
for (const pointer in obj) {
const required = isRequired(pointer, schema);
let subschema = lib.getSchema({ pointer });
console.log("subschema", pointer, subschema, selections);
if (!subschema) {
throw new Error(`"${pointer}" not found in schema`);
}
// if subschema starts with "anyOf" or "oneOf"
if (subschema.anyOf || subschema.oneOf) {
const selected = selections?.[pointer];
if (selected !== undefined) {
subschema = subschema.anyOf ? subschema.anyOf[selected] : subschema.oneOf![selected];
}
}
const value = coerce(obj[pointer], subschema as any, { required });
set(result, pointer.substring(2).replace(/\//g, "."), value);
}
return result;
}
export function coerce(
value: any,
schema: Exclude<JSONSchema, boolean>,
opts?: { required?: boolean }
) {
if (!value && typeof opts?.required === "boolean" && !opts.required) {
return undefined;
}
switch (schema.type) {
case "string":
return String(value);
case "integer":
case "number":
return Number(value);
case "boolean":
return ["true", "1", 1, "on"].includes(value);
case "null":
return null;
}
return value;
}
/**
* normalizes any path to a full json pointer
*
* examples: in -> out
* description -> #/description
* #/description -> #/description
* /description -> #/description
* nested/property -> #/nested/property
* nested.property -> #/nested/property
* nested.property[0] -> #/nested/property/0
* nested.property[0].name -> #/nested/property/0/name
* @param path
*/
export function normalizePath(path: string) {
return path.startsWith("#/")
? path
: `#/${path.replace(/#?\/?/, "").replace(/\./g, "/").replace(/\[/g, "/").replace(/\]/g, "")}`;
}
export function prefixPointer(pointer: string, prefix: string) {
return pointer.replace("#/", `#/${prefix}/`).replace(/\/\//g, "/");
}
export function getParentPointer(pointer: string) {
return pointer.substring(0, pointer.lastIndexOf("/"));
}
export function isRequired(pointer: string, schema: JSONSchema, data?: any) {
if (pointer === "#/") {
return false;
}
const lib = new Draft2019(schema as any);
const childSchema = lib.getSchema({ pointer, data });
if (typeof childSchema === "object" && ("const" in childSchema || "enum" in childSchema)) {
return true;
}
const parentPointer = getParentPointer(pointer);
const parentSchema = lib.getSchema({ pointer: parentPointer, data });
const required = parentSchema?.required?.includes(pointer.split("/").pop()!);
/*console.log("isRequired", {
pointer,
parentPointer,
parent: parentSchema ? JSON.parse(JSON.stringify(parentSchema)) : null,
required
});*/
return !!required;
}
type TType = JSONSchemaType | JSONSchemaType[] | readonly JSONSchemaType[] | undefined;
export function isType(_type: TType, _compare: TType) {
if (!_type || !_compare) return false;
const type = Array.isArray(_type) ? _type : [_type];
const compare = Array.isArray(_compare) ? _compare : [_compare];
return compare.some((t) => type.includes(t));
}
export function getLabel(name: string, schema: JSONSchema) {
if (typeof schema === "object" && "title" in schema) return schema.title;
const label = name.includes("/") ? (name.split("/").pop() ?? "") : name;
return autoFormatString(label);
}
export function getMultiSchema(schema: JSONSchema): Exclude<JSONSchema, boolean>[] | undefined {
if (!schema || typeof schema !== "object") return;
return (schema.anyOf ?? schema.oneOf) as any;
}
export function getMultiSchemaMatched(
schema: JsonSchema,
data: any
): [number, Exclude<JSONSchema, boolean>[], Exclude<JSONSchema, boolean> | undefined] {
const multiSchema = getMultiSchema(schema);
if (!multiSchema) return [-1, [], undefined];
const index = multiSchema.findIndex((subschema) => {
const lib = new Draft2019(subschema as any);
return lib.validate(data, subschema).length === 0;
});
if (index === -1) return [-1, multiSchema, undefined];
return [index, multiSchema, multiSchema[index]];
}
export function removeKeyRecursively<Given extends object>(obj: Given, keyToRemove: string): Given {
if (Array.isArray(obj)) {
return obj.map((item) => removeKeyRecursively(item, keyToRemove)) as any;
} else if (typeof obj === "object" && obj !== null) {
return Object.fromEntries(
Object.entries(obj)
.filter(([key]) => key !== keyToRemove)
.map(([key, value]) => [key, removeKeyRecursively(value, keyToRemove)])
) as any;
}
return obj;
}

View File

@@ -1,11 +0,0 @@
import { type Schema as JsonSchema, type OutputUnit, Validator } from "@cfworker/json-schema";
import type { Validator as TValidator } from "json-schema-form-react";
export class CfValidator implements TValidator<OutputUnit> {
async validate(schema: JsonSchema, data: any) {
const result = new Validator(schema).validate(data);
return result.errors;
}
}
export type { OutputUnit };

View File

@@ -1,11 +0,0 @@
import type { ValueError } from "@sinclair/typebox/value";
import { type TSchema, Value } from "core/utils";
import type { Validator } from "json-schema-form-react";
export class TypeboxValidator implements Validator<ValueError> {
async validate(schema: TSchema, data: any) {
return Value.Check(schema, data) ? [] : [...Value.Errors(schema, data)];
}
}
export type { ValueError };