Importing RJSF lib with first application

This commit is contained in:
2025-04-07 22:54:05 +02:00
parent f93a59b27e
commit 661841ceef
19 changed files with 662 additions and 24 deletions

View File

@@ -26,12 +26,14 @@
"@rjsf/mui": "^5.24.1",
"@rjsf/utils": "^5.24.1",
"@rjsf/validator-ajv8": "^5.24.1",
"lodash": "^4.17.21",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.30.0",
"react-router": "^7.0.2"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/node": "^18.16.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
@@ -3011,6 +3013,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mdast": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",

View File

@@ -22,12 +22,14 @@
"@rjsf/mui": "^5.24.1",
"@rjsf/utils": "^5.24.1",
"@rjsf/validator-ajv8": "^5.24.1",
"lodash": "^4.17.21",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hook-form": "^7.30.0",
"react-router": "^7.0.2"
},
"devDependencies": {
"@types/lodash": "^4.17.16",
"@types/node": "^18.16.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",

View File

@@ -23,7 +23,8 @@ import { ForgotPassword } from "./components/auth/ForgotPassword";
import { UpdatePassword } from "./components/auth/UpdatePassword";
import { Header } from "./components";
import {Hub} from "./pages/hub";
import { Hub } from "./pages/hub";
import { CreateFirm } from "./pages/hub/CreateFirm";
function App() {
return (
@@ -55,6 +56,7 @@ function App() {
)}
>
<Route path="/hub" element={ <Hub /> } />
<Route path="/hub/create-firm" element={ <CreateFirm /> } />
</Route>
<Route index element={<h1>HOME</h1>} />
<Route path="/login" element={<Login />} />

View File

@@ -0,0 +1,65 @@
import validator from "@rjsf/validator-ajv8";
import Form from "@rjsf/mui";
import { RegistryFieldsType, RegistryWidgetsType } from "@rjsf/utils";
import { useEffect, useState } from "react";
import { jsonschemaProvider } from "../providers/jsonschema-provider";
import { useForm } from "@refinedev/core";
import CrudTextWidget from "./widgets/crud-text-widget";
import UnionEnumField from "./fields/union-enum";
type Props = {
schemaName: string,
resource: string,
id?: string,
//onSubmit: (data: IChangeEvent, event: FormEvent<any>) => void
}
const customWidgets: RegistryWidgetsType = {
TextWidget: CrudTextWidget
};
const customFields: RegistryFieldsType = {
AnyOfField: UnionEnumField
}
export const CrudForm: React.FC<Props> = ({schemaName, resource, id}) => {
const { onFinish, query, formLoading } = useForm({
resource: resource,
action: id === undefined ? "create" : "edit",
redirect: "show",
id,
});
const record = query?.data?.data;
const [formData, setFormData] = useState(record);
const [schema, setSchema] = useState({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSchema = async () => {
try {
const resourceSchema = await jsonschemaProvider.getResourceSchema(schemaName);
setSchema(resourceSchema);
setLoading(false);
} catch (error) {
console.error('Error fetching data:', error);
setLoading(false);
}
};
fetchSchema();
}, []);
return (
<Form
schema={schema}
formData={record}
onChange={(e) => setFormData(e.formData)}
onSubmit={(e) => onFinish(e.formData)}
validator={validator}
omitExtraData={true}
widgets={customWidgets}
fields={customFields}
/>
)
}

View File

@@ -0,0 +1,100 @@
import {ERRORS_KEY, FieldProps, FormContextType, getUiOptions, RJSFSchema, StrictRJSFSchema} from "@rjsf/utils";
import { getDefaultRegistry } from "@rjsf/core";
import UnionEnumWidget from "../widgets/union-enum";
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import omit from 'lodash/omit';
const {
fields: { AnyOfField },
} = getDefaultRegistry();
export default function UnionEnumField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: FieldProps
) {
const {
name,
disabled = false,
errorSchema = {},
formContext,
formData,
onChange,
onBlur,
onFocus,
registry,
schema,
uiSchema,
options,
idSchema,
} = props;
getDefaultRegistry<T,S,F>().widgets
const enumOptions: any[] = []
if (options.length == 2 && (options[0].type == "null" || options[1].type == "null")) {
const { SchemaField: _SchemaField } = registry.fields;
let opt_schema = {...schema}
delete(opt_schema.anyOf)
if (options[0].type == "null") {
opt_schema = {...opt_schema, ...options[1]}
} else if (options[1].type == "null") {
opt_schema = {...opt_schema, ...options[0]}
}
return <_SchemaField {...props} schema={opt_schema} uiSchema={uiSchema} />
}
for (let opt of options) {
if (!opt.hasOwnProperty('enum')) {
return (<AnyOfField {...props} />)
}
for (let val of opt.enum) {
enumOptions.push({
title: val,
value: val,
type: opt.title
})
}
}
const { globalUiOptions, schemaUtils } = registry;
const {
placeholder,
autofocus,
autocomplete,
title = schema.title,
...uiOptions
} = getUiOptions(uiSchema, globalUiOptions);
const rawErrors = get(errorSchema, ERRORS_KEY, []);
const fieldErrorSchema = omit(errorSchema, [ERRORS_KEY]);
const displayLabel = schemaUtils.getDisplayLabel(schema, uiSchema, globalUiOptions);
return (
<UnionEnumWidget
id={`${idSchema.$id}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
name={`${name}${schema.oneOf ? '__oneof_select' : '__anyof_select'}`}
schema={schema}
uiSchema={uiSchema}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
disabled={disabled || isEmpty(options)}
multiple={false}
rawErrors={rawErrors}
errorSchema={fieldErrorSchema}
value={formData}
options={{enumOptions}}
registry={registry}
formContext={formContext}
placeholder={placeholder}
autocomplete={autocomplete}
autofocus={autofocus}
label={title ?? name}
hideLabel={!displayLabel}
/>
);
}

View File

@@ -0,0 +1,15 @@
import {FormContextType, getTemplate, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils";
import ForeignKeyWidget from "./foreign-key";
export default function CrudTextWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
if (props.schema.hasOwnProperty("foreign_key")) {
return (<ForeignKeyWidget {...props} />);
} else {
const { options, registry } = props;
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>('BaseInputTemplate', registry, options);
return <BaseInputTemplate {...props} />;
}
}

View File

@@ -0,0 +1,79 @@
import {FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps} from '@rjsf/utils';
import { Autocomplete } from "@mui/material";
import { useState, useEffect } from "react";
import TextField from "@mui/material/TextField";
import {BaseRecord, useList, useOne} from "@refinedev/core";
type ForeignKeySchema = RJSFSchema & {
foreign_key?: {
reference: {
resource: string,
label: string
}
}
}
export default function ForeignKeyWidget<T = any, S extends ForeignKeySchema = ForeignKeySchema, F extends FormContextType = any>(
props: WidgetProps<T, S, F>
) {
if (props.schema.foreign_key === undefined) {
return;
}
const resource = props.schema.foreign_key.reference.resource
const labelField = props.schema.foreign_key.reference.label
const valueResult = useOne({
resource: resource,
id: props.value != null ? props.value : undefined
});
const empty_option: BaseRecord = {
id: undefined
}
empty_option[labelField] = "(None)"
const [inputValue, setInputValue] = useState<string>("");
const [selectedValue, setSelectedValue] = useState(valueResult.data?.data || null);
const [debouncedInputValue, setDebouncedInputValue] = useState<string>(inputValue);
useEffect(() => {
const handler = setTimeout(() => setDebouncedInputValue(inputValue), 300); // Adjust debounce delay as needed
return () => clearTimeout(handler);
}, [inputValue]);
const listResult = useList({
resource: resource,
pagination: { current: 1, pageSize: 10 },
filters: [{ field: "name", operator: "contains", value: debouncedInputValue }],
sorters: [{ field: "name", order: "asc" }],
});
const options = listResult.data?.data || [];
if (! props.required) {
options.unshift(empty_option);
}
const isLoading = listResult.isLoading || valueResult.isLoading;
if(! selectedValue && valueResult.data) {
setSelectedValue(valueResult.data?.data)
}
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue ? newValue : empty_option);
props.onChange(newValue ? newValue.id : null);
return true;
}}
//inputValue={inputValue}
onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
options={options}
getOptionLabel={(option) => option ? option[labelField] : ""}
loading={isLoading}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
};

View File

@@ -0,0 +1,55 @@
import {EnumOptionsType, FormContextType, RJSFSchema, StrictRJSFSchema, WidgetProps} from "@rjsf/utils";
import {useState} from "react";
import Autocomplete from "@mui/material/Autocomplete";
import TextField from "@mui/material/TextField";
import {UIOptionsType} from "@rjsf/utils/src/types";
type CrudEnumOptionsType<S extends StrictRJSFSchema = RJSFSchema> = EnumOptionsType<S> & {
type: string,
title: string
}
interface CrudEnumWidgetProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
extends WidgetProps<T, S, F> {
options: NonNullable<UIOptionsType<T, S, F>> & {
/** The enum options list for a type that supports them */
enumOptions?: CrudEnumOptionsType<S>[];
};
}
export default function UnionEnumWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
props: CrudEnumWidgetProps<T, S, F>
) {
const {
options,
value,
} = props;
const [selectedValue, setSelectedValue] = useState<CrudEnumOptionsType | null>(null);
if (! selectedValue && value && options.enumOptions) {
for (const opt of options.enumOptions){
if (opt.value == value) {
setSelectedValue(opt);
break;
}
}
}
if (options.enumOptions !== undefined) {
return (
<Autocomplete
value={selectedValue}
onChange={(event, newValue) => {
setSelectedValue(newValue);
props.onChange(newValue?.value);
}}
options={options.enumOptions}
groupBy={(option) => option.type}
getOptionLabel={(option) => option.title}
renderInput={(params) => (
<TextField {...params} label={ props.label } variant="outlined" />
)}
/>
);
}
}

View File

@@ -0,0 +1,188 @@
import { JSONSchema7Definition } from "json-schema";
import { RJSFSchema } from '@rjsf/utils';
const API_URL = "/api/v1";
export const jsonschemaProvider = {
getResourceSchema: async (resourceName: string): Promise<RJSFSchema> => {
return buildResource(await getJsonschema(), resourceName)
}
};
let rawSchema: RJSFSchema;
const getJsonschema = async (): Promise<RJSFSchema> => {
if (rawSchema === undefined) {
const response = await fetch(`${API_URL}/openapi.json`,);
rawSchema = await response.json();
}
return rawSchema;
}
function buildResource(rawSchemas: RJSFSchema, resourceName: string) {
let resource;
resource = structuredClone(rawSchemas.components.schemas[resourceName]);
resource.components = { schemas: {} };
for (let prop_name in resource.properties) {
let prop = resource.properties[prop_name];
if (is_reference(prop)) {
resolveReference(rawSchemas, resource, prop);
} else if (is_union(prop)) {
const union = prop.hasOwnProperty("oneOf") ? prop.oneOf : prop.anyOf;
for (let i in union) {
if (is_reference(union[i])) {
resolveReference(rawSchemas, resource, union[i]);
}
}
} else if (is_enum(prop)) {
for (let i in prop.allOf) {
if (is_reference(prop.allOf[i])) {
resolveReference(rawSchemas, resource, prop.allOf[i]);
}
}
} else if (is_array(prop) && is_reference(prop.items)) {
resolveReference(rawSchemas, resource, prop.items);
}
}
return resource;
}
function resolveReference(rawSchemas: RJSFSchema, resource: any, prop_reference: any) {
const subresourceName = get_reference_name(prop_reference);
const subresource = buildResource(rawSchemas, subresourceName);
resource.components.schemas[subresourceName] = subresource;
for (let subsubresourceName in subresource.components.schemas) {
if (! resource.components.schemas.hasOwnProperty(subsubresourceName)) {
resource.components.schemas[subsubresourceName] = subresource.components.schemas[subsubresourceName];
}
}
}
function changePropertiesOrder(resource: any) {
let created_at;
let updated_at;
let new_properties: any = {};
for (let prop_name in resource.properties) {
if (prop_name == 'created_at') {
created_at = resource.properties[prop_name];
} else if (prop_name == 'updated_at') {
updated_at = resource.properties[prop_name];
} else {
new_properties[prop_name] = resource.properties[prop_name];
}
}
if (created_at) {
new_properties['created_at'] = created_at;
}
if (updated_at) {
new_properties['updated_at'] = updated_at;
}
resource.properties = new_properties
}
function is_object(prop: any) {
return prop.hasOwnProperty('properties')
}
function is_reference(prop: any) {
return prop.hasOwnProperty('$ref');
}
function is_array(prop: any) {
return prop.hasOwnProperty('items');
}
function is_union(prop: any) {
return prop.hasOwnProperty('oneOf') || prop.hasOwnProperty('anyOf');
}
function is_enum(prop: any) {
return prop.hasOwnProperty('enum');
}
function get_reference_name(prop: any) {
return prop['$ref'].substring(prop['$ref'].lastIndexOf('/')+1);
}
function has_descendant(rawSchemas:RJSFSchema, resource: RJSFSchema, property_name: string): boolean {
if (is_array(resource)) {
return property_name == 'items';
} else if (is_object(resource)) {
return property_name in resource.properties!;
} else if (is_reference(resource)) {
let subresourceName = get_reference_name(resource);
return has_descendant(rawSchemas, buildResource(rawSchemas, subresourceName), property_name);
} else if (is_union(resource)) {
const union = resource.hasOwnProperty("oneOf") ? resource.oneOf : resource.anyOf;
if (union !== undefined) {
for (const ref of union) {
return has_descendant(rawSchemas, ref as RJSFSchema, property_name)
}
}
} else if (is_enum(resource)) {
for (const ref of resource.allOf!) {
return has_descendant(rawSchemas, ref as RJSFSchema, property_name);
}
}
throw new Error("Jsonschema format not implemented in property finder");
}
function get_descendant(rawSchemas: RJSFSchema, resource: RJSFSchema, property_name: string): RJSFSchema {
if (is_array(resource) && property_name == 'items') {
return resource.items as RJSFSchema;
} else if (is_object(resource) && resource.properties !== undefined && property_name in resource.properties!) {
return resource.properties[property_name] as RJSFSchema;
} else if (is_reference(resource)) {
let subresourceName = get_reference_name(resource);
let subresource = buildResource(rawSchemas, subresourceName);
return get_descendant(rawSchemas, subresource, property_name);
} else if (is_union(resource)) {
for (const ref of resource.oneOf!) {
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
}
}
} else if (is_enum(resource)) {
for (const ref of resource.allOf!) {
if (has_descendant(rawSchemas, ref as RJSFSchema, property_name)) {
return get_descendant(rawSchemas, ref as RJSFSchema, property_name);
}
}
}
throw new Error("property not found or Jsonschema format not implemented");
}
function path_exists(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): boolean{
const pointFirstPosition = path.indexOf('.')
if (pointFirstPosition == -1) {
return has_descendant(rawSchemas, resource, path);
}
return has_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition))
&& path_exists(
rawSchemas,
get_descendant(rawSchemas, resource, path.substring(0, pointFirstPosition)),
path.substring(pointFirstPosition + 1)
);
}
function get_property_by_path(rawSchemas: RJSFSchema, resource: RJSFSchema, path: string): RJSFSchema {
const pointFirstPosition = path.indexOf('.')
if (pointFirstPosition == -1) {
return get_descendant(rawSchemas, resource, path);
}
return get_property_by_path(
rawSchemas,
get_descendant(
rawSchemas,
resource,
path.substring(0, pointFirstPosition)
),
path.substring(pointFirstPosition + 1)
);
}

View File

@@ -1,9 +1,7 @@
import { CrudForm } from "../../lib/crud/components/crud-form";
export const CreateFirm = () => {
return (
<div>
<h1>Create Firm</h1>
</div>
<CrudForm schemaName={"FirmCreate"} resource={"firms"} />
)
}

View File

@@ -1,13 +1,13 @@
import {Button} from "@mui/material";
import { Button } from "@mui/material";
import { Link } from "react-router";
export const Hub = () => {
return (
<div>
<h1>HUB</h1>
<p>List of managed firms</p>
<p>List of firm you're working atx</p>
<p><Button>Create a new firm</Button></p>
<Link to="/hub/create-firm" ><Button >Create a new firm</Button></Link>
</div>
);
};