diff --git a/src/builder/JsonSchemaBuilder.ts b/src/builder/JsonSchemaBuilder.ts index fdf2714..8c4656e 100644 --- a/src/builder/JsonSchemaBuilder.ts +++ b/src/builder/JsonSchemaBuilder.ts @@ -1,4 +1,5 @@ import {JsonSchema, JsonSchemaType, Format} from "../types"; +import {deleteNestedPropertyByPath, generatePath, updateNestedObjectByPath} from "../utils"; /** @@ -28,6 +29,13 @@ export class JsonSchemaBuilder { this.schema.properties[name] = propSchema; return this; } + addNestedProperty(name: string, propSchema: JsonSchema): JsonSchemaBuilder { + if (!this.schema.properties) { + this.schema.properties = {}; + } + this.schema = updateNestedObjectByPath(this.schema, name, propSchema); + return this; + } addRequired(...args: string[]): JsonSchemaBuilder { if (!this.schema.required) { @@ -158,9 +166,7 @@ export class JsonSchemaBuilder { } deleteProperty(name: string): JsonSchemaBuilder { - if (this.schema.properties && this.schema.properties[name]) { - delete this.schema.properties[name]; - } + this.schema = deleteNestedPropertyByPath(this.schema, name) return this; } @@ -172,9 +178,7 @@ export class JsonSchemaBuilder { } editProperty(name: string, propSchema: JsonSchema): JsonSchemaBuilder { - if (this.schema.properties && this.schema.properties[name]) { - this.schema.properties[name] = propSchema; - } + this.schema = updateNestedObjectByPath(this.schema, name, propSchema) return this; } diff --git a/src/components/AddFieldModal.tsx b/src/components/AddFieldModal.tsx index 4d6df6d..9d46d37 100644 --- a/src/components/AddFieldModal.tsx +++ b/src/components/AddFieldModal.tsx @@ -8,8 +8,9 @@ import {JsonSchemaType} from "../types"; import {JsonSchemaField} from "../fields/JsonSchemaField"; import Select from "@mui/material/Select"; import {Add} from "@mui/icons-material"; +import {generatePath} from "../utils"; -const AddFieldModal = () => { +const AddFieldModal = ({ parentPath }) => { const [open, setOpen] = React.useState(false); const [name, setName] = React.useState(null); const [type, setType] = React.useState(null); @@ -33,7 +34,7 @@ const AddFieldModal = () => { dispatch({ type: "ADD_PROPERTY", payload: { - name: field.getName() || 'newField', + name: generatePath(parentPath, field.getName() || 'newField'), schema: field.getSchema(), }, }); @@ -41,7 +42,7 @@ const AddFieldModal = () => { dispatch({ type: "ADD_REQUIRED", payload: { - name: field.getName() || 'newField', + name: generatePath(parentPath, field.getName() || 'newField'), }, }) } diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index b6c3338..f6f43f1 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -24,7 +24,7 @@ import { Tooltip, Typography } from "@mui/material"; -import {getFieldId, getSchemaFormatFromSchema} from "../utils"; +import {generatePath, getFieldId, getSchemaFormatFromSchema} from "../utils"; import {DataVisualizationType} from "../types"; import {SchemaAction, useSchema} from "../providers/SchemaProvider"; import Form from "@rjsf/mui"; @@ -35,11 +35,11 @@ type Props = { schema: RJSFSchema; data: unknown; name: string; + path?: string; } - // TODO: refactor -const renderHeader = ({icon, schema, onDelete, name}: { +const renderHeader = ({icon, schema, onDelete, name, path}: { icon?: React.ReactNode, schema: RJSFSchema, name: string, @@ -63,15 +63,16 @@ const renderHeader = ({icon, schema, onDelete, name}: { Edit {name} Field
{ - dispatch({type: "UPDATE_PROPERTY", payload: {name, schema: formData}}) + handleEdit(dispatch, path, formData) setShowEditModal(false); }} schema={field?.getBuilderSchema()} formData={schema} validator={validator}/> setShowPreviewModal(false)}> - {name} Field {onDelete && setShowDeleteConfirmationModal(true)}>} + {name} Field {onDelete && + setShowDeleteConfirmationModal(true)}>} setShowEditModal(true)} @@ -79,7 +80,7 @@ const renderHeader = ({icon, schema, onDelete, name}: { - + @@ -142,43 +143,43 @@ const handleEdit = (dispatch: React.Dispatch, name: string, schema dispatch({type: "UPDATE_PROPERTY", payload: {name, schema}}); } -const SchemaPreview = ({schema, data, name}: Props) => { +const SchemaPreview = ({schema, data, name, path}: Props) => { const FormPreview = getSchemaFormatFromSchema(schema, SchemaPreview) return (
- +
) }; -SchemaPreview.String = function String({schema, data, name}: DataVisualizationType) { +SchemaPreview.String = function String({schema, path, data, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
- {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})}
); }; -SchemaPreview.Number = function Number({schema, name}: DataVisualizationType) { +SchemaPreview.Number = function Number({schema, path, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
- {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})}
); }; -SchemaPreview.Boolean = function BooleanVisualization({schema, name}: DataVisualizationType) { +SchemaPreview.Boolean = function BooleanVisualization({schema, path, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
- {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})}
); }; -SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVisualizationType) { +SchemaPreview.Object = function ObjectVisualization({schema, path, data, name}: DataVisualizationType) { const {dispatch} = useSchema(); const properties = Object.keys(schema?.properties || {}) @@ -192,27 +193,31 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi {renderHeader({ name, + path, schema, icon: , collapse: open, onCollapse: handleCollapse, - onDelete: () => handleDelete(dispatch, name) + onDelete: () => handleDelete(dispatch, path) })} Properties - + {open !== undefined && {!open ? : }} {properties?.length > 0 ? properties?.map((property) => ( - + <> + + )) : ( Click on button to add properties @@ -224,14 +229,14 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi ); }; -SchemaPreview.Array = function ArrayVisualization({schema, data, name}: DataVisualizationType) { +SchemaPreview.Array = function ArrayVisualization({schema, path, data, name}: DataVisualizationType) { const {dispatch} = useSchema(); return ( <> - {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name,path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})} diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index b01d40c..e936f2e 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -94,7 +94,7 @@ const schemaReducer = (state: JsonSchema, action: SchemaAction): JsonSchema => { switch (action.type) { case "ADD_PROPERTY": - builder.addProperty(action.payload.name, action.payload.schema!); + builder.addNestedProperty(action.payload.name, action.payload.schema!); break; case "UPDATE_PROPERTY": builder.editProperty(action.payload.name, action.payload.schema!); diff --git a/src/types.ts b/src/types.ts index cf7cde2..2fa9f34 100644 --- a/src/types.ts +++ b/src/types.ts @@ -75,4 +75,7 @@ export type DataVisualizationType = { data?: object; schema: RJSFSchema; name?: string + path?: string } + +export type NestedObject = { [key: string]: any }; diff --git a/src/utils.ts b/src/utils.ts index df24226..92f62a5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import {RJSFSchema} from "@rjsf/utils"; -import {DataVisualizationType} from "./types"; +import {DataVisualizationType, NestedObject} from "./types"; export const getSchemaFormatFromSchema = ( schema: RJSFSchema, @@ -48,4 +48,62 @@ export const getFieldId = (schema: RJSFSchema) => { if (schema?.type === 'number' || schema?.type === 'integer') return 'NUMBER'; if (schema?.type === 'object') return 'OBJECT'; if (schema?.type === 'array') return 'ARRAY'; -} \ No newline at end of file +} + +export const generatePath = (parentPath: string = '', fieldName: string): string => { + let path = parentPath; + if (path?.length > 0) path += '.'; + path += fieldName; + return path; +} + +export const accessToObjectFieldByPath = (object: object, path: string) => { + return path.split('.').reduce((o, i) => o[i], object) +} + +export const updateNestedObjectByPath = (obj: NestedObject, path: string, value: any): NestedObject => { + const keys = path.split('.'); + const newObject = {...obj}; + + let current = newObject; + keys.forEach((key, index) => { + if (index === keys.length - 1) { + current[key] = value; + } else { + current[key] = current[key] ? {...current[key]} : {}; + current = current[key]; + } + }); + + return newObject; +} + +export const deleteNestedPropertyByPath = (obj: NestedObject, path: string): NestedObject => { + const keys = path.split('.'); + const newObject = {...obj}; + + if (keys.length === 0) { + return newObject; + } + + let current = newObject; + const stack = []; + + for (let i = 0; i < keys.length - 1; i++) { + stack.push(current); + current[keys[i]] = {...current[keys[i]]}; + current = current[keys[i]]; + } + + delete current[keys[keys.length - 1]]; + + for (let i = keys.length - 2; i >= 0; i--) { + const key = keys[i]; + current = stack.pop(); + if (Object.keys(current[key]).length === 0) { + delete current[key]; + } + } + + return newObject; +}