public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
import type { Schema } from "@cfworker/json-schema";
import Form from "@rjsf/core";
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
import { forwardRef, useId, useImperativeHandle, useRef, useState } from "react";
//import { JsonSchemaValidator } from "./JsonSchemaValidator";
import { fields as Fields } from "./fields";
import { templates as Templates } from "./templates";
import { widgets as Widgets } from "./widgets";
import "./styles.css";
import { filterKeys } from "core/utils";
import { cloneDeep } from "lodash-es";
import { RJSFTypeboxValidator } from "./typebox/RJSFTypeboxValidator";
const validator = new RJSFTypeboxValidator();
// @todo: don't import FormProps, instead, copy it here instead of "any"
export type JsonSchemaFormProps = any & {
schema: RJSFSchema | Schema;
uiSchema?: any;
direction?: "horizontal" | "vertical";
onChange?: (value: any) => void;
};
export type JsonSchemaFormRef = {
formData: () => any;
validateForm: () => boolean;
cancel: () => void;
};
export const JsonSchemaForm = forwardRef<JsonSchemaFormRef, JsonSchemaFormProps>(
(
{
className,
direction = "vertical",
schema,
onChange,
uiSchema,
templates,
fields,
widgets,
...props
},
ref
) => {
const formRef = useRef<Form<any, RJSFSchema, any>>(null);
const id = useId();
const [value, setValue] = useState<any>(props.formData);
const onSubmit = ({ formData }: any, e) => {
e.preventDefault();
console.log("Data submitted: ", formData);
props.onSubmit?.(formData);
return false;
};
const handleChange = ({ formData }: any, e) => {
const clean = JSON.parse(JSON.stringify(formData));
//console.log("Data changed: ", clean, JSON.stringify(formData, null, 2));
onChange?.(clean);
setValue(clean);
};
useImperativeHandle(
ref,
() => ({
formData: () => value,
validateForm: () => formRef.current!.validateForm(),
cancel: () => formRef.current!.reset()
}),
[value]
);
const _uiSchema: UiSchema = {
...uiSchema,
"ui:globalOptions": {
...uiSchema?.["ui:globalOptions"],
enableMarkdownInDescription: true
},
"ui:submitButtonOptions": {
norender: true
}
};
const _fields: any = {
...Fields,
...fields
};
const _templates: any = {
...Templates,
...templates
};
const _widgets: any = {
...Widgets,
...widgets
};
//console.log("schema", schema, removeTitleFromSchema(schema));
return (
<Form
tagName="div"
idSeparator="--"
idPrefix={id}
{...props}
ref={formRef}
className={["json-form", direction, className].join(" ")}
showErrorList={false}
schema={schema as RJSFSchema}
fields={_fields}
templates={_templates}
widgets={_widgets}
uiSchema={_uiSchema}
onChange={handleChange}
onSubmit={onSubmit}
validator={validator as any}
/>
);
}
);
function removeTitleFromSchema(schema: any): any {
// Create a deep copy of the schema using lodash
const newSchema = cloneDeep(schema);
function removeTitle(schema: any): void {
if (typeof schema !== "object" || schema === null) return;
// Remove title if present
// biome-ignore lint/performance/noDelete: <explanation>
delete schema.title;
// Check nested schemas in anyOf, allOf, and oneOf
const nestedKeywords = ["anyOf", "allOf", "oneOf"];
nestedKeywords.forEach((keyword) => {
if (Array.isArray(schema[keyword])) {
schema[keyword].forEach((nestedSchema: any) => {
removeTitle(nestedSchema);
});
}
});
// Recursively remove title from properties
if (schema.properties && typeof schema.properties === "object") {
Object.values(schema.properties).forEach((propertySchema: any) => {
removeTitle(propertySchema);
});
}
// Recursively remove title from items
if (schema.items) {
if (Array.isArray(schema.items)) {
schema.items.forEach((itemSchema: any) => {
removeTitle(itemSchema);
});
} else {
removeTitle(schema.items);
}
}
}
removeTitle(newSchema);
return newSchema;
}

View File

@@ -0,0 +1,121 @@
import { type OutputUnit, Validator } from "@cfworker/json-schema";
import type {
CustomValidator,
ErrorSchema,
ErrorTransformer,
FormContextType,
RJSFSchema,
RJSFValidationError,
StrictRJSFSchema,
UiSchema,
ValidationData,
ValidatorType
} from "@rjsf/utils";
import { toErrorSchema } from "@rjsf/utils";
import get from "lodash-es/get";
function removeUndefinedKeys(obj: any): any {
if (!obj) return obj;
if (typeof obj === "object") {
Object.keys(obj).forEach((key) => {
if (obj[key] === undefined) {
delete obj[key];
} else if (typeof obj[key] === "object") {
removeUndefinedKeys(obj[key]);
}
});
}
if (Array.isArray(obj)) {
return obj.filter((item) => item !== undefined);
}
return obj;
}
function onlyKeepMostSpecific(errors: OutputUnit[]) {
const mostSpecific = errors.filter((error) => {
return !errors.some((other) => {
return error !== other && other.instanceLocation.startsWith(error.instanceLocation);
});
});
return mostSpecific;
}
const debug = true;
const validate = true;
export class JsonSchemaValidator<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
> implements ValidatorType
{
// @ts-ignore
rawValidation<Result extends OutputUnit = OutputUnit>(schema: S, formData?: T) {
if (!validate) return { errors: [], validationError: null };
debug && console.log("JsonSchemaValidator.rawValidation", schema, formData);
const validator = new Validator(schema as any);
const validation = validator.validate(removeUndefinedKeys(formData));
const specificErrors = onlyKeepMostSpecific(validation.errors);
return { errors: specificErrors, validationError: null as any };
}
validateFormData(
formData: T | undefined,
schema: S,
customValidate?: CustomValidator,
transformErrors?: ErrorTransformer,
uiSchema?: UiSchema
): ValidationData<T> {
if (!validate) return { errors: [], errorSchema: {} as any };
debug &&
console.log(
"JsonSchemaValidator.validateFormData",
formData,
schema,
customValidate,
transformErrors,
uiSchema
);
const { errors } = this.rawValidation(schema, formData);
debug && console.log("errors", { errors });
const transformedErrors = errors
//.filter((error) => error.keyword !== "properties")
.map((error) => {
const schemaLocation = error.keywordLocation.replace(/^#\/?/, "").split("/").join(".");
const propertyError = get(schema, schemaLocation);
const errorText = `${error.error.replace(/\.$/, "")}${propertyError ? ` "${propertyError}"` : ""}`;
//console.log(error, schemaLocation, get(schema, schemaLocation));
return {
name: error.keyword,
message: errorText,
property: "." + error.instanceLocation.replace(/^#\/?/, "").split("/").join("."),
schemaPath: error.keywordLocation,
stack: error.error
};
});
debug && console.log("transformed", transformedErrors);
return {
errors: transformedErrors,
errorSchema: toErrorSchema(transformedErrors)
} as any;
}
toErrorList(errorSchema?: ErrorSchema<T>, fieldPath?: string[]): RJSFValidationError[] {
debug && console.log("JsonSchemaValidator.toErrorList", errorSchema, fieldPath);
return [];
}
isValid(schema: S, formData: T | undefined, rootSchema: S): boolean {
if (!validate) return true;
debug && console.log("JsonSchemaValidator.isValid", schema, formData, rootSchema);
return this.rawValidation(schema, formData).errors.length === 0;
}
}

View File

@@ -0,0 +1,32 @@
import type { FieldProps } from "@rjsf/utils";
import { JsonEditor } from "../../../code/JsonEditor";
import { Label } from "../templates/FieldTemplate";
// @todo: move editor to lazy loading component
export default function JsonField({
formData,
onChange,
disabled,
readonly,
...props
}: FieldProps) {
const value = JSON.stringify(formData, null, 2);
function handleChange(data) {
try {
onChange(JSON.parse(data));
} catch (err) {
console.error(err);
}
}
const isDisabled = disabled || readonly;
const id = props.idSchema.$id;
return (
<div className="flex flex-col gap-2">
<Label label={props.name} id={id} />
<JsonEditor value={value} editable={!isDisabled} onChange={handleChange} />
</div>
);
}

View File

@@ -0,0 +1,26 @@
import type { FieldProps } from "@rjsf/utils";
import { LiquidJsEditor } from "../../../code/LiquidJsEditor";
import { Label } from "../templates/FieldTemplate";
// @todo: move editor to lazy loading component
export default function LiquidJsField({
formData,
onChange,
disabled,
readonly,
...props
}: FieldProps) {
function handleChange(data) {
onChange(data);
}
const isDisabled = disabled || readonly;
const id = props.idSchema.$id;
return (
<div className="flex flex-col gap-2">
<Label label={props.name} id={id} />
<LiquidJsEditor value={formData} editable={!isDisabled} onChange={handleChange} />
</div>
);
}

View File

@@ -0,0 +1,307 @@
import {
ANY_OF_KEY,
ERRORS_KEY,
type FieldProps,
type FormContextType,
ONE_OF_KEY,
type RJSFSchema,
type StrictRJSFSchema,
TranslatableString,
type UiSchema,
deepEquals,
getDiscriminatorFieldFromSchema,
getUiOptions,
getWidget,
mergeSchemas
} from "@rjsf/utils";
import get from "lodash-es/get";
import isEmpty from "lodash-es/isEmpty";
import omit from "lodash-es/omit";
import { Component } from "react";
import { twMerge } from "tailwind-merge";
import { Label } from "../templates/FieldTemplate";
/** Type used for the state of the `AnyOfField` component */
type AnyOfFieldState<S extends StrictRJSFSchema = RJSFSchema> = {
/** The currently selected option */
selectedOption: number;
/** The option schemas after retrieving all $refs */
retrievedOptions: S[];
};
/** The `AnyOfField` component is used to render a field in the schema that is an `anyOf`, `allOf` or `oneOf`. It tracks
* the currently selected option and cleans up any irrelevant data in `formData`.
*
* @param props - The `FieldProps` for this template
*/
class MultiSchemaField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
> extends Component<FieldProps<T, S, F>, AnyOfFieldState<S>> {
/** Constructs an `AnyOfField` with the given `props` to initialize the initially selected option in state
*
* @param props - The `FieldProps` for this template
*/
constructor(props: FieldProps<T, S, F>) {
super(props);
const {
formData,
options,
registry: { schemaUtils }
} = this.props;
// cache the retrieved options in state in case they have $refs to save doing it later
//console.log("multi schema", { formData, options, props });
const retrievedOptions = options.map((opt: S) => schemaUtils.retrieveSchema(opt, formData));
this.state = {
retrievedOptions,
selectedOption: this.getMatchingOption(0, formData, retrievedOptions)
};
}
/** React lifecycle method that is called when the props and/or state for this component is updated. It recomputes the
* currently selected option based on the overall `formData`
*
* @param prevProps - The previous `FieldProps` for this template
* @param prevState - The previous `AnyOfFieldState` for this template
*/
override componentDidUpdate(
prevProps: Readonly<FieldProps<T, S, F>>,
prevState: Readonly<AnyOfFieldState>
) {
const { formData, options, idSchema } = this.props;
const { selectedOption } = this.state;
let newState = this.state;
if (!deepEquals(prevProps.options, options)) {
const {
registry: { schemaUtils }
} = this.props;
// re-cache the retrieved options in state in case they have $refs to save doing it later
const retrievedOptions = options.map((opt: S) =>
schemaUtils.retrieveSchema(opt, formData)
);
newState = { selectedOption, retrievedOptions };
}
if (!deepEquals(formData, prevProps.formData) && idSchema.$id === prevProps.idSchema.$id) {
const { retrievedOptions } = newState;
const matchingOption = this.getMatchingOption(selectedOption, formData, retrievedOptions);
if (prevState && matchingOption !== selectedOption) {
newState = { selectedOption: matchingOption, retrievedOptions };
}
}
if (newState !== this.state) {
this.setState(newState);
}
}
/** Determines the best matching option for the given `formData` and `options`.
*
* @param formData - The new formData
* @param options - The list of options to choose from
* @return - The index of the `option` that best matches the `formData`
*/
getMatchingOption(selectedOption: number, formData: T | undefined, options: S[]) {
const {
schema,
registry: { schemaUtils }
} = this.props;
const discriminator = getDiscriminatorFieldFromSchema<S>(schema);
const option = schemaUtils.getClosestMatchingOption(
formData,
options,
selectedOption,
discriminator
);
return option;
}
/** Callback handler to remember what the currently selected option is. In addition to that the `formData` is updated
* to remove properties that are not part of the newly selected option schema, and then the updated data is passed to
* the `onChange` handler.
*
* @param option - The new option value being selected
*/
onOptionChange = (option?: string) => {
const { selectedOption, retrievedOptions } = this.state;
const { formData, onChange, registry } = this.props;
console.log("onOptionChange", { state: { selectedOption, retrievedOptions }, option });
const { schemaUtils } = registry;
const intOption = option !== undefined ? Number.parseInt(option, 10) : -1;
if (intOption === selectedOption) {
return;
}
const newOption = intOption >= 0 ? retrievedOptions[intOption] : undefined;
const oldOption = selectedOption >= 0 ? retrievedOptions[selectedOption] : undefined;
let newFormData = schemaUtils.sanitizeDataForNewSchema(newOption, oldOption, formData);
if (newFormData && newOption) {
// Call getDefaultFormState to make sure defaults are populated on change. Pass "excludeObjectChildren"
// so that only the root objects themselves are created without adding undefined children properties
newFormData = schemaUtils.getDefaultFormState(
newOption,
newFormData,
"excludeObjectChildren"
) as T;
}
onChange(newFormData, undefined, this.getFieldId());
this.setState({ selectedOption: intOption });
};
getFieldId() {
const { idSchema, schema } = this.props;
return `${idSchema.$id}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`;
}
/** Renders the `AnyOfField` selector along with a `SchemaField` for the value of the `formData`
*/
override render() {
const {
name,
disabled = false,
errorSchema = {},
formContext,
onBlur,
onFocus,
registry,
schema,
uiSchema,
readonly
} = this.props;
const { widgets, fields, translateString, globalUiOptions, schemaUtils } = registry;
const { SchemaField: _SchemaField } = fields;
const { selectedOption, retrievedOptions } = this.state;
const {
widget = "select",
placeholder,
autofocus,
autocomplete,
title = schema.title,
flexDirection,
wrap,
...uiOptions
} = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
/* console.log("multi schema", {
name,
schema,
uiSchema,
uiOptions,
globalUiOptions,
disabled,
flexDirection,
props: this.props
}); */
const Widget = getWidget<T, S, F>({ type: "number" }, widget, widgets);
const rawErrors = get(errorSchema, ERRORS_KEY, []);
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
const option = selectedOption >= 0 ? retrievedOptions[selectedOption] || null : null;
let optionSchema: S | undefined | null;
if (option) {
// merge top level required field
const { required } = schema;
// Merge in all the non-oneOf/anyOf properties and also skip the special ADDITIONAL_PROPERTY_FLAG property
optionSchema = required ? (mergeSchemas({ required }, option) as S) : option;
}
// First we will check to see if there is an anyOf/oneOf override for the UI schema
let optionsUiSchema: UiSchema<T, S, F>[] = [];
if (ONE_OF_KEY in schema && uiSchema && ONE_OF_KEY in uiSchema) {
if (Array.isArray(uiSchema[ONE_OF_KEY])) {
optionsUiSchema = uiSchema[ONE_OF_KEY];
} else {
console.warn(`uiSchema.oneOf is not an array for "${title || name}"`);
}
} else if (ANY_OF_KEY in schema && uiSchema && ANY_OF_KEY in uiSchema) {
if (Array.isArray(uiSchema[ANY_OF_KEY])) {
optionsUiSchema = uiSchema[ANY_OF_KEY];
} else {
console.warn(`uiSchema.anyOf is not an array for "${title || name}"`);
}
}
// Then we pick the one that matches the selected option index, if one exists otherwise default to the main uiSchema
let optionUiSchema = uiSchema;
if (selectedOption >= 0 && optionsUiSchema.length > selectedOption) {
optionUiSchema = optionsUiSchema[selectedOption];
}
const translateEnum: TranslatableString = title
? TranslatableString.TitleOptionPrefix
: TranslatableString.OptionPrefix;
const translateParams = title ? [title] : [];
const enumOptions = retrievedOptions.map((opt: { title?: string }, index: number) => {
// Also see if there is an override title in the uiSchema for each option, otherwise use the title from the option
const { title: uiTitle = opt.title } = getUiOptions<T, S, F>(optionsUiSchema[index]);
return {
label:
uiTitle || translateString(translateEnum, translateParams.concat(String(index + 1))),
value: index
};
});
//console.log("sub component", { optionSchema, props: this.props, optionUiSchema });
const SubComponent = optionSchema && (
// @ts-ignore
<_SchemaField
{...this.props}
schema={optionSchema}
uiSchema={{
...optionUiSchema,
"ui:options": {
...optionUiSchema?.["ui:options"],
hideLabel: true
}
}}
/>
);
return (
<div
className={twMerge(
"panel multischema flex",
flexDirection === "row" ? "flex-row gap-3" : "flex-col gap-2"
)}
>
<div className="flex flex-row gap-2 items-center panel-select">
<Label
label={this.props.name}
required={this.props.required}
id={this.getFieldId()}
/>
<Widget
id={this.getFieldId()}
name={`${name}${schema.oneOf ? "__oneof_select" : "__anyof_select"}`}
schema={{ type: "number", default: 0 } as S}
onChange={this.onOptionChange}
onBlur={onBlur}
onFocus={onFocus}
disabled={disabled || isEmpty(enumOptions) || readonly}
multiple={false}
rawErrors={rawErrors}
errorSchema={fieldErrorSchema}
value={selectedOption >= 0 ? selectedOption : undefined}
options={{ enumOptions, ...uiOptions }}
registry={registry}
formContext={formContext}
placeholder={placeholder}
autocomplete={autocomplete}
autofocus={autofocus}
label={""}
hideLabel={!displayLabel}
/>
</div>
{wrap ? <fieldset>{SubComponent}</fieldset> : SubComponent}
</div>
);
}
}
export default MultiSchemaField;

View File

@@ -0,0 +1,10 @@
import JsonField from "./JsonField";
import LiquidJsField from "./LiquidJsField";
import MultiSchemaField from "./MultiSchemaField";
export const fields = {
AnyOfField: MultiSchemaField,
OneOfField: MultiSchemaField,
JsonField,
LiquidJsField
};

View File

@@ -0,0 +1,264 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
.json-form {
@apply flex flex-col flex-grow;
/* dirty fix preventing the first fieldset to wrap */
&.mute-root {
& > div > div > div > fieldset:first-child {
@apply border-none p-0;
}
}
&:not(.fieldset-alternative) {
fieldset {
@apply flex flex-grow flex-col gap-3.5 border border-solid border-muted p-3 rounded;
.title-field {
@apply bg-primary/10 px-3 text-sm font-medium py-1 rounded-full;
align-self: flex-start;
}
}
}
/* alternative */
&.fieldset-alternative {
fieldset {
@apply flex flex-grow flex-col gap-3.5;
&:has(> legend) {
@apply mt-3 border-l-4 border-solid border-muted/50 p-3 pb-0 pt-0;
}
.title-field {
@apply bg-muted/50 text-sm font-medium py-1 table ml-[-14px] pl-4 pr-3 mb-3 mt-3;
align-self: flex-start;
}
}
.multischema {
@apply mt-3;
fieldset {
margin-top: 0 !important;
}
}
}
&.hide-required-mark {
.control-label span.required {
display: none;
}
}
.form-group {
@apply flex flex-col gap-1;
&:not(.field) {
@apply flex-grow;
}
/* hide empty description if markdown is enabled */
.field-description:has(> span:empty) {
display: none;
}
.control-label span.required {
@apply ml-1 opacity-50;
}
&.field.has-error {
@apply text-red-500;
.control-label {
@apply font-bold;
}
.error-detail:not(:only-child) {
@apply font-bold list-disc pl-6;
}
.error-detail:only-child {
@apply font-bold;
}
}
}
.field-description {
@apply text-primary/70 text-sm;
}
/* input but not radio */
input:not([type="radio"]):not([type="checkbox"]) {
@apply flex bg-muted/40 h-11 rounded-md outline-none;
@apply py-2.5 px-4;
width: 100%;
&:not([disabled]):not([readonly]) {
@apply focus:outline-none focus:ring-2 focus:bg-muted focus:ring-zinc-500 focus:border-transparent transition-all;
}
&[disabled], &[readonly] {
@apply bg-muted/50 text-primary/50 cursor-not-allowed;
}
}
textarea {
@apply flex bg-muted/40 focus:bg-muted rounded-md outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all disabled:bg-muted/50 disabled:text-primary/50;
@apply py-2.5 px-4;
width: 100%;
}
.checkbox {
label, label > span {
@apply flex flex-row gap-2;
}
}
select {
@apply bg-muted/40 focus:bg-muted rounded-md py-2.5 pr-4 pl-2.5 outline-none focus:outline-none focus:ring-2 focus:ring-zinc-500 focus:border-transparent transition-all;
@apply disabled:bg-muted/70 disabled:text-primary/70;
@apply w-full border-r-8 border-r-transparent;
&:not([multiple]) {
@apply h-11;
}
&[multiple] {
option {
@apply py-1.5 px-2.5 bg-transparent;
&:checked {
@apply bg-primary/20;
}
}
}
}
.btn {
@apply w-5 h-5 bg-amber-500;
}
.field-radio-group {
@apply flex flex-row gap-2;
}
&.noborder-first-fieldset {
fieldset#root {
@apply border-none p-0;
}
}
&.horizontal {
.form-group {
@apply flex-row gap-2;
}
.form-control, .panel {
@apply flex-grow;
}
.control-label {
@apply w-32 flex h-11 items-center;
}
input {
width: auto;
}
fieldset#root {
@apply gap-6;
}
fieldset.object-field {
@apply gap-2;
}
.additional-children {
.checkbox {
@apply w-full;
}
}
}
&.hide-multi-labels {
.control-label {
display: none;
}
}
.multischema {
.form-control {
@apply flex-shrink;
}
}
.panel {
/*@apply flex flex-col gap-2;*/
/*.control-label { display: none; }*/
& > .field-radio-group {
@apply flex flex-row gap-3;
.radio, .radio-inline {
@apply text-sm border-b border-b-transparent;
@apply font-mono text-primary/70;
input {
@apply appearance-none;
}
&.checked {
@apply border-b-primary/70 text-primary;
}
}
}
/* :not(.panel-select) .control-label {
display: none;
} */
.panel-select select {
@apply py-1 pr-1 pl-1.5 text-sm;
@apply h-auto w-auto;
}
}
&.legacy {
/* first fieldset */
& > .form-group.field-object>div>fieldset {
@apply border-none p-0;
}
.row {
display: flex;
flex-direction: row;
gap: 1rem;
}
.col-xs-5 {
display: flex;
width: 50%;
}
.form-additional {
fieldset {
/* padding: 0;
border: none; */
/* legend {
display: none;
} */
}
&.additional-start {
> label {
display: none;
}
/* > label + div > fieldset:first-child {
display: none;
} */
}
}
.field-object + .field-object {
@apply mt-3 pt-4 border-t border-muted;
}
.panel>.field-object>label {
display: none;
}
}
}

View File

@@ -0,0 +1,80 @@
import type {
ArrayFieldTemplateItemType,
FormContextType,
RJSFSchema,
StrictRJSFSchema,
} from "@rjsf/utils";
import { type CSSProperties, Children, cloneElement, isValidElement } from "react";
import { twMerge } from "tailwind-merge";
/** The `ArrayFieldItemTemplate` component is the template used to render an items of an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldItemTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(props: ArrayFieldTemplateItemType<T, S, F>) {
const {
children,
className,
disabled,
hasToolbar,
hasMoveDown,
hasMoveUp,
hasRemove,
hasCopy,
index,
onCopyIndexClick,
onDropIndexClick,
onReorderClick,
readonly,
registry,
uiSchema,
} = props;
const { CopyButton, MoveDownButton, MoveUpButton, RemoveButton } =
registry.templates.ButtonTemplates;
return (
<div className={twMerge("flex flex-row w-full overflow-hidden", className)}>
{hasToolbar && (
<div className="flex flex-col gap-1 p-1 mr-2">
{(hasMoveUp || hasMoveDown) && (
<MoveUpButton
disabled={disabled || readonly || !hasMoveUp}
onClick={onReorderClick(index, index - 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{(hasMoveUp || hasMoveDown) && (
<MoveDownButton
disabled={disabled || readonly || !hasMoveDown}
onClick={onReorderClick(index, index + 1)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasCopy && (
<CopyButton
disabled={disabled || readonly}
onClick={onCopyIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
{hasRemove && (
<RemoveButton
disabled={disabled || readonly}
onClick={onDropIndexClick(index)}
uiSchema={uiSchema}
registry={registry}
/>
)}
</div>
)}
<div className="flex flex-col flex-grow">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,100 @@
import {
type ArrayFieldTemplateItemType,
type ArrayFieldTemplateProps,
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
getTemplate,
getUiOptions
} from "@rjsf/utils";
import { cloneElement } from "react";
/** The `ArrayFieldTemplate` component is the template used to render all items in an array.
*
* @param props - The `ArrayFieldTemplateItemType` props for the component
*/
export default function ArrayFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ArrayFieldTemplateProps<T, S, F>) {
const {
canAdd,
className,
disabled,
idSchema,
uiSchema,
items,
onAddClick,
readonly,
registry,
required,
schema,
title
} = props;
const uiOptions = getUiOptions<T, S, F>(uiSchema);
const ArrayFieldDescriptionTemplate = getTemplate<"ArrayFieldDescriptionTemplate", T, S, F>(
"ArrayFieldDescriptionTemplate",
registry,
uiOptions
);
const ArrayFieldItemTemplate = getTemplate<"ArrayFieldItemTemplate", T, S, F>(
"ArrayFieldItemTemplate",
registry,
uiOptions
);
const ArrayFieldTitleTemplate = getTemplate<"ArrayFieldTitleTemplate", T, S, F>(
"ArrayFieldTitleTemplate",
registry,
uiOptions
);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton }
} = registry.templates;
return (
<fieldset className={className} id={idSchema.$id}>
<ArrayFieldTitleTemplate
idSchema={idSchema}
title={uiOptions.title || title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
<ArrayFieldDescriptionTemplate
idSchema={idSchema}
description={uiOptions.description || schema.description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
{items && items.length > 0 && (
<div className="flex flex-col gap-3 array-items">
{items.map(
({ key, children, ...itemProps }: ArrayFieldTemplateItemType<T, S, F>) => {
const newChildren = cloneElement(children, {
...children.props,
name: undefined,
title: undefined
});
return (
<ArrayFieldItemTemplate key={key} {...itemProps} children={newChildren} />
);
}
)}
</div>
)}
{canAdd && (
<AddButton
className="array-item-add"
onClick={onAddClick}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
);
}

View File

@@ -0,0 +1,120 @@
import {
type BaseInputTemplateProps,
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
ariaDescribedByIds,
examplesId,
getInputProps
} from "@rjsf/utils";
import { type ChangeEvent, type FocusEvent, useCallback } from "react";
import { Label } from "./FieldTemplate";
/** The `BaseInputTemplate` is the template to use to render the basic `<input>` component for the `core` theme.
* It is used as the template for rendering many of the <input> based widgets that differ by `type` and callbacks only.
* It can be customized/overridden for other themes or individual implementations as needed.
*
* @param props - The `WidgetProps` for this template
*/
export default function BaseInputTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: BaseInputTemplateProps<T, S, F>) {
const {
id,
name, // remove this from ...rest
value,
readonly,
disabled,
autofocus,
onBlur,
onFocus,
onChange,
onChangeOverride,
options,
schema,
uiSchema,
formContext,
registry,
rawErrors,
type,
hideLabel, // remove this from ...rest
hideError, // remove this from ...rest
...rest
} = props;
// Note: since React 15.2.0 we can't forward unknown element attributes, so we
// exclude the "options" and "schema" ones here.
if (!id) {
console.log("No id for", props);
throw new Error(`no id for props ${JSON.stringify(props)}`);
}
const inputProps = {
...rest,
...getInputProps<T, S, F>(schema, type, options)
};
let inputValue;
if (inputProps.type === "number" || inputProps.type === "integer") {
inputValue = value || value === 0 ? value : "";
} else {
inputValue = value == null ? "" : value;
}
const _onChange = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
onChange(value === "" ? options.emptyValue : value),
[onChange, options]
);
const _onBlur = useCallback(
({ target }: FocusEvent<HTMLInputElement>) => onBlur(id, target && target.value),
[onBlur, id]
);
const _onFocus = useCallback(
({ target }: FocusEvent<HTMLInputElement>) => onFocus(id, target && target.value),
[onFocus, id]
);
const shouldHideLabel =
!props.label ||
// @ts-ignore
uiSchema["ui:options"]?.hideLabel ||
props.options?.hideLabel ||
props.hideLabel;
return (
<>
{!shouldHideLabel && <Label label={props.label} required={props.required} id={id} />}
<input
id={id}
name={id}
className="form-control"
readOnly={readonly}
disabled={disabled}
autoFocus={autofocus}
value={inputValue}
{...inputProps}
placeholder={props.label}
list={schema.examples ? examplesId<T>(id) : undefined}
onChange={onChangeOverride || _onChange}
onBlur={_onBlur}
onFocus={_onFocus}
aria-describedby={ariaDescribedByIds<T>(id, !!schema.examples)}
/>
{Array.isArray(schema.examples) && (
<datalist key={`datalist_${id}`} id={examplesId<T>(id)}>
{(schema.examples as string[])
.concat(
schema.default && !schema.examples.includes(schema.default)
? ([schema.default] as string[])
: []
)
.map((example: any) => {
return <option key={example} value={example} />;
})}
</datalist>
)}
</>
);
}

View File

@@ -0,0 +1,29 @@
import { TbArrowDown, TbArrowUp, TbPlus, TbTrash } from "react-icons/tb";
import { Button } from "../../../buttons/Button";
import { IconButton } from "../../../buttons/IconButton";
export const AddButton = ({ onClick, disabled, ...rest }) => (
<div className="flex flex-row">
<Button onClick={onClick} disabled={disabled} IconLeft={TbPlus}>
Add
</Button>
</div>
);
export const RemoveButton = ({ onClick, disabled, ...rest }) => (
<div className="flex flex-row">
<IconButton onClick={onClick} disabled={disabled} Icon={TbTrash} />
</div>
);
export const MoveUpButton = ({ onClick, disabled, ...rest }) => (
<div className="flex flex-row">
<IconButton onClick={onClick} disabled={disabled} Icon={TbArrowUp} />
</div>
);
export const MoveDownButton = ({ onClick, disabled, ...rest }) => (
<div className="flex flex-row">
<IconButton onClick={onClick} disabled={disabled} Icon={TbArrowDown} />
</div>
);

View File

@@ -0,0 +1,95 @@
import {
type FieldTemplateProps,
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
getTemplate,
getUiOptions
} from "@rjsf/utils";
import { ucFirstAll, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
import { twMerge } from "tailwind-merge";
const REQUIRED_FIELD_SYMBOL = "*";
export type LabelProps = {
/** The label for the field */
label?: string;
/** A boolean value stating if the field is required */
required?: boolean;
/** The id of the input field being labeled */
id?: string;
};
/** Renders a label for a field
*
* @param props - The `LabelProps` for this component
*/
export function Label(props: LabelProps) {
const { label, required, id } = props;
if (!label) {
return null;
}
return (
<label className="control-label" htmlFor={id}>
{ucFirstAllSnakeToPascalWithSpaces(label)}
{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}
</label>
);
}
/** The `FieldTemplate` component is the template used by `SchemaField` to render any field. It renders the field
* content, (label, description, children, errors and help) inside of a `WrapIfAdditional` component.
*
* @param props - The `FieldTemplateProps` for this component
*/
export function FieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: FieldTemplateProps<T, S, F>) {
const {
id,
label,
children,
errors,
help,
description,
hidden,
required,
displayLabel,
registry,
uiSchema
} = props;
const uiOptions = getUiOptions(uiSchema, registry.globalUiOptions);
//console.log("field---", uiOptions);
const WrapIfAdditionalTemplate = getTemplate<"WrapIfAdditionalTemplate", T, S, F>(
"WrapIfAdditionalTemplate",
registry,
uiOptions
);
if (hidden) {
return <div className="hidden">{children}</div>;
}
//console.log("FieldTemplate", props);
return (
<WrapIfAdditionalTemplate {...props}>
{/*<Label label={label} required={required} id={id} />*/}
<div className="flex flex-col flex-grow gap-2 additional">
<div
className={twMerge(
"flex flex-grow additional-children",
uiOptions.flexDirection === "row"
? "flex-row items-center gap-3"
: "flex-col flex-grow gap-2"
)}
>
{children}
</div>
{displayLabel && description ? description : null}
</div>
{errors}
{help}
</WrapIfAdditionalTemplate>
);
}

View File

@@ -0,0 +1,101 @@
import {
type FormContextType,
type ObjectFieldTemplatePropertyType,
type ObjectFieldTemplateProps,
type RJSFSchema,
type StrictRJSFSchema,
canExpand,
descriptionId,
getTemplate,
getUiOptions,
titleId
} from "@rjsf/utils";
/** The `ObjectFieldTemplate` is the template to use to render all the inner properties of an object along with the
* title and description if available. If the object is expandable, then an `AddButton` is also rendered after all
* the properties.
*
* @param props - The `ObjectFieldTemplateProps` for this component
*/
export default function ObjectFieldTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: ObjectFieldTemplateProps<T, S, F>) {
const {
description,
disabled,
formData,
idSchema,
onAddClick,
properties,
readonly,
registry,
required,
schema,
title,
uiSchema
} = props;
const options = getUiOptions<T, S, F>(uiSchema);
const TitleFieldTemplate = getTemplate<"TitleFieldTemplate", T, S, F>(
"TitleFieldTemplate",
registry,
options
);
const DescriptionFieldTemplate = getTemplate<"DescriptionFieldTemplate", T, S, F>(
"DescriptionFieldTemplate",
registry,
options
);
/* if (properties.length === 0) {
return null;
} */
const _canExpand = canExpand(schema, uiSchema, formData);
if (properties.length === 0 && !_canExpand) {
return null;
}
//console.log("multi:properties", uiSchema, props, options);
// Button templates are not overridden in the uiSchema
const {
ButtonTemplates: { AddButton }
} = registry.templates;
return (
<>
<fieldset id={idSchema.$id} className="object-field">
{title && (
<TitleFieldTemplate
id={titleId<T>(idSchema)}
title={title}
required={required}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{description && (
<DescriptionFieldTemplate
id={descriptionId<T>(idSchema)}
description={description}
schema={schema}
uiSchema={uiSchema}
registry={registry}
/>
)}
{properties.map((prop: ObjectFieldTemplatePropertyType) => prop.content)}
{_canExpand && (
<AddButton
className="object-property-expand"
onClick={onAddClick(schema)}
disabled={disabled || readonly}
uiSchema={uiSchema}
registry={registry}
/>
)}
</fieldset>
</>
);
}

View File

@@ -0,0 +1,22 @@
import type { FormContextType, RJSFSchema, StrictRJSFSchema, TitleFieldProps } from "@rjsf/utils";
import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
const REQUIRED_FIELD_SYMBOL = "*";
/** The `TitleField` is the template to use to render the title of a field
*
* @param props - The `TitleFieldProps` for this component
*/
export default function TitleField<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: TitleFieldProps<T, S, F>) {
const { id, title, required } = props;
return (
<legend id={id} className="title-field">
{ucFirstAllSnakeToPascalWithSpaces(title)}
{/*{required && <span className="required">{REQUIRED_FIELD_SYMBOL}</span>}*/}
</legend>
);
}

View File

@@ -0,0 +1,84 @@
import {
ADDITIONAL_PROPERTY_FLAG,
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
TranslatableString,
type WrapIfAdditionalTemplateProps
} from "@rjsf/utils";
import { useState } from "react";
/** The `WrapIfAdditional` component is used by the `FieldTemplate` to rename, or remove properties that are
* part of an `additionalProperties` part of a schema.
*
* @param props - The `WrapIfAdditionalProps` for this component
*/
export default function WrapIfAdditionalTemplate<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>(props: WrapIfAdditionalTemplateProps<T, S, F>) {
const {
id,
classNames,
style,
disabled,
label,
onKeyChange,
onDropPropertyClick,
readonly,
required,
schema,
children,
uiSchema,
registry
} = props;
const { templates, translateString } = registry;
// Button templates are not overridden in the uiSchema
const { RemoveButton } = templates.ButtonTemplates;
const keyLabel = translateString(TranslatableString.KeyLabel, [label]);
const additional = ADDITIONAL_PROPERTY_FLAG in schema;
const [expanded, setExpanded] = useState(true);
if (!additional) {
return (
<div className={classNames} style={style}>
{children}
</div>
);
}
return (
<div className={classNames} style={style}>
<div className="flex flex-col">
<fieldset>
<legend className="flex flex-row justify-between gap-3">
<RemoveButton
className="array-item-remove btn-block"
style={{ border: "0" }}
disabled={disabled || readonly}
onClick={onDropPropertyClick(label)}
uiSchema={uiSchema}
registry={registry}
/>
<div className="form-group">
<input
className="form-control"
type="text"
id={`${id}-key`}
onBlur={(event) => onKeyChange(event.target.value)}
defaultValue={label}
/>
</div>
<button onClick={() => setExpanded((prev) => !prev)}>
{expanded ? "collapse" : "expand"}
</button>
</legend>
{expanded && (
<div className="form-additional additional-start form-group">{children}</div>
)}
</fieldset>
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import ArrayFieldItemTemplate from "./ArrayFieldItemTemplate";
import ArrayFieldTemplate from "./ArrayFieldTemplate";
import BaseInputTemplate from "./BaseInputTemplate";
import * as ButtonTemplates from "./ButtonTemplates";
import { FieldTemplate } from "./FieldTemplate";
import ObjectFieldTemplate from "./ObjectFieldTemplate";
import TitleFieldTemplate from "./TitleFieldTemplate";
import WrapIfAdditionalTemplate from "./WrapIfAdditionalTemplate";
export const templates = {
ButtonTemplates,
ArrayFieldItemTemplate,
ArrayFieldTemplate,
FieldTemplate,
TitleFieldTemplate,
ObjectFieldTemplate,
BaseInputTemplate,
WrapIfAdditionalTemplate
};

View File

@@ -0,0 +1,76 @@
import { Check, Errors } from "core/utils";
import { FromSchema } from "./from-schema";
import type {
CustomValidator,
ErrorTransformer,
RJSFSchema,
RJSFValidationError,
StrictRJSFSchema,
UiSchema,
ValidationData,
ValidatorType
} from "@rjsf/utils";
import { toErrorSchema } from "@rjsf/utils";
const validate = true;
export class RJSFTypeboxValidator<T = any, S extends StrictRJSFSchema = RJSFSchema>
implements ValidatorType
{
// @ts-ignore
rawValidation(schema: S, formData?: T) {
if (!validate) {
return { errors: [], validationError: null as any };
}
const tbSchema = FromSchema(schema as unknown);
//console.log("--validation", tbSchema, formData);
if (Check(tbSchema, formData)) {
return { errors: [], validationError: null as any };
}
return {
errors: [...Errors(tbSchema, formData)],
validationError: null as any
};
}
validateFormData(
formData: T | undefined,
schema: S,
customValidate?: CustomValidator,
transformErrors?: ErrorTransformer,
uiSchema?: UiSchema
): ValidationData<T> {
const { errors } = this.rawValidation(schema, formData);
const transformedErrors = errors.map((error) => {
const schemaLocation = error.path.substring(1).split("/").join(".");
return {
name: "any",
message: error.message,
property: "." + schemaLocation,
schemaPath: error.path,
stack: error.message
};
});
return {
errors: transformedErrors,
errorSchema: toErrorSchema(transformedErrors)
} as any;
}
isValid(schema: S, formData: T | undefined, rootSchema: S): boolean {
const validation = this.rawValidation(schema, formData);
return validation.errors.length === 0;
}
toErrorList(): RJSFValidationError[] {
return [];
}
}

View File

@@ -0,0 +1,299 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/prototypes
The MIT License (MIT)
Copyright (c) 2017-2024 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---------------------------------------------------------------------------*/
import * as Type from "@sinclair/typebox";
// ------------------------------------------------------------------
// Schematics
// ------------------------------------------------------------------
const IsExact = (value: unknown, expect: unknown) => value === expect;
const IsSValue = (value: unknown): value is SValue =>
Type.ValueGuard.IsString(value) ||
Type.ValueGuard.IsNumber(value) ||
Type.ValueGuard.IsBoolean(value);
const IsSEnum = (value: unknown): value is SEnum =>
Type.ValueGuard.IsObject(value) &&
Type.ValueGuard.IsArray(value.enum) &&
value.enum.every((value) => IsSValue(value));
const IsSAllOf = (value: unknown): value is SAllOf =>
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf);
const IsSAnyOf = (value: unknown): value is SAnyOf =>
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf);
const IsSOneOf = (value: unknown): value is SOneOf =>
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf);
const IsSTuple = (value: unknown): value is STuple =>
Type.ValueGuard.IsObject(value) &&
IsExact(value.type, "array") &&
Type.ValueGuard.IsArray(value.items);
const IsSArray = (value: unknown): value is SArray =>
Type.ValueGuard.IsObject(value) &&
IsExact(value.type, "array") &&
!Type.ValueGuard.IsArray(value.items) &&
Type.ValueGuard.IsObject(value.items);
const IsSConst = (value: unknown): value is SConst =>
// biome-ignore lint: reason
Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]);
const IsSString = (value: unknown): value is SString =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "string");
const IsSNumber = (value: unknown): value is SNumber =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "number");
const IsSInteger = (value: unknown): value is SInteger =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer");
const IsSBoolean = (value: unknown): value is SBoolean =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean");
const IsSNull = (value: unknown): value is SBoolean =>
Type.ValueGuard.IsObject(value) && IsExact(value.type, "null");
const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value);
// prettier-ignore
// biome-ignore format:
const IsSObject = (value: unknown): value is SObject => Type.ValueGuard.IsObject(value) && IsExact(value.type, 'object') && IsSProperties(value.properties) && (value.required === undefined || Type.ValueGuard.IsArray(value.required) && value.required.every((value: unknown) => Type.ValueGuard.IsString(value)))
type SValue = string | number | boolean;
type SEnum = Readonly<{ enum: readonly SValue[] }>;
type SAllOf = Readonly<{ allOf: readonly unknown[] }>;
type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>;
type SOneOf = Readonly<{ oneOf: readonly unknown[] }>;
type SProperties = Record<PropertyKey, unknown>;
type SObject = Readonly<{
type: "object";
properties: SProperties;
required?: readonly string[];
}>;
type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>;
type SArray = Readonly<{ type: "array"; items: unknown }>;
type SConst = Readonly<{ const: SValue }>;
type SString = Readonly<{ type: "string" }>;
type SNumber = Readonly<{ type: "number" }>;
type SInteger = Readonly<{ type: "integer" }>;
type SBoolean = Readonly<{ type: "boolean" }>;
type SNull = Readonly<{ type: "null" }>;
// ------------------------------------------------------------------
// FromRest
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromRest<T extends readonly unknown[], Acc extends Type.TSchema[] = []> = (
// biome-ignore lint: reason
T extends readonly [infer L extends unknown, ...infer R extends unknown[]]
? TFromSchema<L> extends infer S extends Type.TSchema
? TFromRest<R, [...Acc, S]>
: TFromRest<R, [...Acc]>
: Acc
)
function FromRest<T extends readonly unknown[]>(T: T): TFromRest<T> {
return T.map((L) => FromSchema(L)) as never;
}
// ------------------------------------------------------------------
// FromEnumRest
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromEnumRest<T extends readonly SValue[], Acc extends Type.TSchema[] = []> = (
T extends readonly [infer L extends SValue, ...infer R extends SValue[]]
? TFromEnumRest<R, [...Acc, Type.TLiteral<L>]>
: Acc
)
function FromEnumRest<T extends readonly SValue[]>(T: T): TFromEnumRest<T> {
return T.map((L) => Type.Literal(L)) as never;
}
// ------------------------------------------------------------------
// AllOf
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromAllOf<T extends SAllOf> = (
TFromRest<T['allOf']> extends infer Rest extends Type.TSchema[]
? Type.TIntersectEvaluated<Rest>
: Type.TNever
)
function FromAllOf<T extends SAllOf>(T: T): TFromAllOf<T> {
return Type.IntersectEvaluated(FromRest(T.allOf), T);
}
// ------------------------------------------------------------------
// AnyOf
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromAnyOf<T extends SAnyOf> = (
TFromRest<T['anyOf']> extends infer Rest extends Type.TSchema[]
? Type.TUnionEvaluated<Rest>
: Type.TNever
)
function FromAnyOf<T extends SAnyOf>(T: T): TFromAnyOf<T> {
return Type.UnionEvaluated(FromRest(T.anyOf), T);
}
// ------------------------------------------------------------------
// OneOf
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromOneOf<T extends SOneOf> = (
TFromRest<T['oneOf']> extends infer Rest extends Type.TSchema[]
? Type.TUnionEvaluated<Rest>
: Type.TNever
)
function FromOneOf<T extends SOneOf>(T: T): TFromOneOf<T> {
return Type.UnionEvaluated(FromRest(T.oneOf), T);
}
// ------------------------------------------------------------------
// Enum
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromEnum<T extends SEnum> = (
TFromEnumRest<T['enum']> extends infer Elements extends Type.TSchema[]
? Type.TUnionEvaluated<Elements>
: Type.TNever
)
function FromEnum<T extends SEnum>(T: T): TFromEnum<T> {
return Type.UnionEvaluated(FromEnumRest(T.enum));
}
// ------------------------------------------------------------------
// Tuple
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromTuple<T extends STuple> = (
TFromRest<T['items']> extends infer Elements extends Type.TSchema[]
? Type.TTuple<Elements>
: Type.TTuple<[]>
)
// prettier-ignore
// biome-ignore format:
function FromTuple<T extends STuple>(T: T): TFromTuple<T> {
return Type.Tuple(FromRest(T.items), T) as never
}
// ------------------------------------------------------------------
// Array
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromArray<T extends SArray> = (
TFromSchema<T['items']> extends infer Items extends Type.TSchema
? Type.TArray<Items>
: Type.TArray<Type.TUnknown>
)
// prettier-ignore
// biome-ignore format:
function FromArray<T extends SArray>(T: T): TFromArray<T> {
return Type.Array(FromSchema(T.items), T) as never
}
// ------------------------------------------------------------------
// Const
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
type TFromConst<T extends SConst> = (
Type.Ensure<Type.TLiteral<T['const']>>
)
function FromConst<T extends SConst>(T: T) {
return Type.Literal(T.const, T);
}
// ------------------------------------------------------------------
// Object
// ------------------------------------------------------------------
type TFromPropertiesIsOptional<
K extends PropertyKey,
R extends string | unknown
> = unknown extends R ? true : K extends R ? false : true;
// prettier-ignore
// biome-ignore format:
type TFromProperties<T extends SProperties, R extends string | unknown> = Type.Evaluate<{
-readonly [K in keyof T]: TFromPropertiesIsOptional<K, R> extends true
? Type.TOptional<TFromSchema<T[K]>>
: TFromSchema<T[K]>
}>
// prettier-ignore
// biome-ignore format:
type TFromObject<T extends SObject> = (
TFromProperties<T['properties'], Exclude<T['required'], undefined>[number]> extends infer Properties extends Type.TProperties
? Type.TObject<Properties>
: Type.TObject<{}>
)
function FromObject<T extends SObject>(T: T): TFromObject<T> {
const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce((Acc, K) => {
return {
// biome-ignore lint:
...Acc,
[K]:
// biome-ignore lint: reason
T.required && T.required.includes(K)
? FromSchema(T.properties[K])
: Type.Optional(FromSchema(T.properties[K]))
};
}, {} as Type.TProperties);
if ("additionalProperties" in T) {
return Type.Object(properties, {
additionalProperties: FromSchema(T.additionalProperties)
}) as never;
}
return Type.Object(properties, T) as never;
}
// ------------------------------------------------------------------
// FromSchema
// ------------------------------------------------------------------
// prettier-ignore
// biome-ignore format:
export type TFromSchema<T> = (
T extends SAllOf ? TFromAllOf<T> :
T extends SAnyOf ? TFromAnyOf<T> :
T extends SOneOf ? TFromOneOf<T> :
T extends SEnum ? TFromEnum<T> :
T extends SObject ? TFromObject<T> :
T extends STuple ? TFromTuple<T> :
T extends SArray ? TFromArray<T> :
T extends SConst ? TFromConst<T> :
T extends SString ? Type.TString :
T extends SNumber ? Type.TNumber :
T extends SInteger ? Type.TInteger :
T extends SBoolean ? Type.TBoolean :
T extends SNull ? Type.TNull :
Type.TUnknown
)
/** Parses a TypeBox type from raw JsonSchema */
export function FromSchema<T>(T: T): TFromSchema<T> {
// prettier-ignore
// biome-ignore format:
return (
IsSAllOf(T) ? FromAllOf(T) :
IsSAnyOf(T) ? FromAnyOf(T) :
IsSOneOf(T) ? FromOneOf(T) :
IsSEnum(T) ? FromEnum(T) :
IsSObject(T) ? FromObject(T) :
IsSTuple(T) ? FromTuple(T) :
IsSArray(T) ? FromArray(T) :
IsSConst(T) ? FromConst(T) :
IsSString(T) ? Type.String(T) :
IsSNumber(T) ? Type.Number(T) :
IsSInteger(T) ? Type.Integer(T) :
IsSBoolean(T) ? Type.Boolean(T) :
IsSNull(T) ? Type.Null(T) :
Type.Unknown(T || {})
) as never
}

View File

@@ -0,0 +1,48 @@
import { Switch } from "@mantine/core";
import type { FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps } from "@rjsf/utils";
import { type ChangeEvent, useCallback } from "react";
export function CheckboxWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>({
schema,
uiSchema,
options,
id,
value,
disabled,
readonly,
label,
hideLabel,
autofocus = false,
onBlur,
onFocus,
onChange,
registry,
...props
}: WidgetProps<T, S, F>) {
/*console.log("addprops", value, props, label, {
label,
name: props.name,
hideLabel,
label_lower: label.toLowerCase(),
name_lower: props.name.toLowerCase(),
equals: label.toLowerCase() === props.name.toLowerCase()
});*/
const handleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => onChange(event.target.checked),
[onChange]
);
return (
<Switch
id={id}
onChange={handleChange}
defaultChecked={value}
disabled={disabled || readonly}
label={label.toLowerCase() === props.name.toLowerCase() ? undefined : label}
/>
);
}

View File

@@ -0,0 +1,98 @@
import {
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
type WidgetProps,
ariaDescribedByIds,
enumOptionsDeselectValue,
enumOptionsIsSelected,
enumOptionsSelectValue,
enumOptionsValueForIndex,
optionId
} from "@rjsf/utils";
import { type ChangeEvent, type FocusEvent, useCallback } from "react";
/** The `CheckboxesWidget` is a widget for rendering checkbox groups.
* It is typically used to represent an array of enums.
*
* @param props - The `WidgetProps` for this component
*/
function CheckboxesWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>({
id,
disabled,
options: { inline = false, enumOptions, enumDisabled, emptyValue },
value,
autofocus = false,
readonly,
onChange,
onBlur,
onFocus
}: WidgetProps<T, S, F>) {
const checkboxesValues = Array.isArray(value) ? value : [value];
const handleBlur = useCallback(
({ target }: FocusEvent<HTMLInputElement>) =>
onBlur(id, enumOptionsValueForIndex<S>(target?.value, enumOptions, emptyValue)),
[onBlur, id]
);
const handleFocus = useCallback(
({ target }: FocusEvent<HTMLInputElement>) =>
onFocus(id, enumOptionsValueForIndex<S>(target?.value, enumOptions, emptyValue)),
[onFocus, id]
);
return (
<div className="checkboxes" id={id}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, index) => {
const checked = enumOptionsIsSelected<S>(option.value, checkboxesValues);
const itemDisabled =
Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
onChange(enumOptionsSelectValue<S>(index, checkboxesValues, enumOptions));
} else {
onChange(enumOptionsDeselectValue<S>(index, checkboxesValues, enumOptions));
}
};
const checkbox = (
// biome-ignore lint/correctness/useJsxKeyInIterable: it's wrapped
<span>
<input
type="checkbox"
id={optionId(id, index)}
name={id}
checked={checked}
value={String(index)}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && index === 0}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
aria-describedby={ariaDescribedByIds<T>(id)}
/>
<span>{option.label}</span>
</span>
);
return inline ? (
<label key={index} className={`checkbox-inline ${disabledCls}`}>
{checkbox}
</label>
) : (
<div key={index} className={`checkbox ${disabledCls}`}>
<label>{checkbox}</label>
</div>
);
})}
</div>
);
}
export default CheckboxesWidget;

View File

@@ -0,0 +1,19 @@
import type { WidgetProps } from "@rjsf/utils";
import { useState } from "react";
export default function JsonWidget({ value, onChange, disabled, readonly, ...props }: WidgetProps) {
const [val, setVal] = useState(JSON.stringify(value, null, 2));
function handleChange(e) {
setVal(e.target.value);
try {
onChange(JSON.parse(e.target.value));
} catch (err) {
console.error(err);
}
}
return (
<textarea value={val} rows={10} disabled={disabled || readonly} onChange={handleChange} />
);
}

View File

@@ -0,0 +1,97 @@
import {
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
type WidgetProps,
ariaDescribedByIds,
enumOptionsIsSelected,
enumOptionsValueForIndex,
optionId
} from "@rjsf/utils";
import { type FocusEvent, useCallback } from "react";
/** The `RadioWidget` is a widget for rendering a radio group.
* It is typically used with a string property constrained with enum options.
*
* @param props - The `WidgetProps` for this component
*/
function RadioWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>({
options,
value,
required,
disabled,
readonly,
autofocus = false,
onBlur,
onFocus,
onChange,
id
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, inline, emptyValue } = options;
const handleBlur = useCallback(
({ target: { value } }: FocusEvent<HTMLInputElement>) =>
onBlur(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue)),
[onBlur, id]
);
const handleFocus = useCallback(
({ target: { value } }: FocusEvent<HTMLInputElement>) =>
onFocus(id, enumOptionsValueForIndex<S>(value, enumOptions, emptyValue)),
[onFocus, id]
);
return (
<div className="field-radio-group" id={id}>
{Array.isArray(enumOptions) &&
enumOptions.map((option, i) => {
const checked = enumOptionsIsSelected<S>(option.value, value);
const itemDisabled =
Array.isArray(enumDisabled) && enumDisabled.indexOf(option.value) !== -1;
const disabledCls = disabled || itemDisabled || readonly ? "disabled" : "";
const handleChange = () => onChange(option.value);
const radio = (
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
<span>
<input
type="radio"
id={optionId(id, i)}
checked={checked}
name={id}
required={required}
value={String(i)}
disabled={disabled || itemDisabled || readonly}
autoFocus={autofocus && i === 0}
onChange={handleChange}
onBlur={handleBlur}
onFocus={handleFocus}
aria-describedby={ariaDescribedByIds<T>(id)}
/>
<span>{option.label}</span>
</span>
);
return inline ? (
<label
key={i}
className={`radio-inline ${checked ? "checked" : ""} ${disabledCls}`}
>
{radio}
</label>
) : (
<div key={i} className={`radio ${checked ? "checked" : ""} ${disabledCls}`}>
<label>{radio}</label>
</div>
);
})}
</div>
);
}
export default RadioWidget;

View File

@@ -0,0 +1,105 @@
import {
type FormContextType,
type RJSFSchema,
type StrictRJSFSchema,
type WidgetProps,
ariaDescribedByIds,
enumOptionsIndexForValue,
enumOptionsValueForIndex
} from "@rjsf/utils";
import { type ChangeEvent, type FocusEvent, type SyntheticEvent, useCallback } from "react";
function getValue(event: SyntheticEvent<HTMLSelectElement>, multiple: boolean) {
if (multiple) {
return Array.from((event.target as HTMLSelectElement).options)
.slice()
.filter((o) => o.selected)
.map((o) => o.value);
}
return (event.target as HTMLSelectElement).value;
}
/** The `SelectWidget` is a widget for rendering dropdowns.
* It is typically used with string properties constrained with enum options.
*
* @param props - The `WidgetProps` for this component
*/
function SelectWidget<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any
>({
schema,
id,
options,
value,
required,
disabled,
readonly,
multiple = false,
autofocus = false,
onChange,
onBlur,
onFocus,
placeholder
}: WidgetProps<T, S, F>) {
const { enumOptions, enumDisabled, emptyValue: optEmptyVal } = options;
const emptyValue = multiple ? [] : "";
const handleFocus = useCallback(
(event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onFocus(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
},
[onFocus, id, schema, multiple, enumOptions, optEmptyVal]
);
const handleBlur = useCallback(
(event: FocusEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onBlur(id, enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
},
[onBlur, id, schema, multiple, enumOptions, optEmptyVal]
);
const handleChange = useCallback(
(event: ChangeEvent<HTMLSelectElement>) => {
const newValue = getValue(event, multiple);
return onChange(enumOptionsValueForIndex<S>(newValue, enumOptions, optEmptyVal));
},
[onChange, schema, multiple, enumOptions, optEmptyVal]
);
const selectedIndexes = enumOptionsIndexForValue<S>(value, enumOptions, multiple);
const showPlaceholderOption = !multiple && schema.default === undefined;
return (
<select
id={id}
name={id}
multiple={multiple}
className="form-control"
value={typeof selectedIndexes === "undefined" ? emptyValue : selectedIndexes}
required={required}
disabled={disabled || readonly}
autoFocus={autofocus}
onBlur={handleBlur}
onFocus={handleFocus}
onChange={handleChange}
aria-describedby={ariaDescribedByIds<T>(id)}
>
{showPlaceholderOption && <option value="">{placeholder}</option>}
{Array.isArray(enumOptions) &&
enumOptions.map(({ value, label }, i) => {
const disabled = enumDisabled && enumDisabled.indexOf(value) !== -1;
return (
<option key={i} value={String(i)} disabled={disabled}>
{label}
</option>
);
})}
</select>
);
}
export default SelectWidget;

View File

@@ -0,0 +1,30 @@
import { Label } from "../templates/FieldTemplate";
import { CheckboxWidget } from "./CheckboxWidget";
import CheckboxesWidget from "./CheckboxesWidget";
import JsonWidget from "./JsonWidget";
import RadioWidget from "./RadioWidget";
import SelectWidget from "./SelectWidget";
const WithLabel = (WrappedComponent, kind?: string) => {
return (props) => {
const hideLabel =
!props.label ||
props.uiSchema["ui:options"]?.hideLabel ||
props.options?.hideLabel ||
props.hideLabel;
return (
<>
{!hideLabel && <Label label={props.label} required={props.required} id={props.id} />}
<WrappedComponent {...props} />
</>
);
};
};
export const widgets = {
RadioWidget: RadioWidget,
CheckboxWidget: WithLabel(CheckboxWidget),
SelectWidget: WithLabel(SelectWidget, "select"),
CheckboxesWidget: WithLabel(CheckboxesWidget),
JsonWidget: WithLabel(JsonWidget)
};