From cf986f67dd1f77e5395722fe9b986cb31c4d03fe Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 11:05:28 +0330 Subject: [PATCH 1/8] feat: impelment types ui --- package.json | 1 + pnpm-lock.yaml | 13 +-- src/components/AddFieldModal.tsx | 7 +- src/components/SchemaBuilder.tsx | 44 +++++++--- src/components/SchemaPreview.tsx | 129 ++++++++++++++++++++++++++++ src/fields/containers/ArrayField.ts | 34 ++++++-- src/providers/SchemaProvider.tsx | 80 ++++++++++++++++- src/types.ts | 11 ++- src/utils.ts | 40 +++++++++ 9 files changed, 328 insertions(+), 31 deletions(-) create mode 100644 src/components/SchemaPreview.tsx create mode 100644 src/utils.ts diff --git a/package.json b/package.json index 8309327..d213901 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", "@lit/react": "^1.0.5", + "@mui/icons-material": "^5.15.20", "@mui/material": "^5.15.19", "@rjsf/core": "^5.18.4", "@rjsf/mui": "^5.18.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9598b8c..9368056 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@lit/react': specifier: ^1.0.5 version: 1.0.5(@types/react@18.3.3) + '@mui/icons-material': + specifier: ^5.15.20 + version: 5.15.20(@mui/material@5.15.19)(@types/react@18.3.3)(react@18.3.1) '@mui/material': specifier: ^5.15.19 version: 5.15.19(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -22,7 +25,7 @@ dependencies: version: 5.18.4(@rjsf/utils@5.18.4)(react@18.3.1) '@rjsf/mui': specifier: ^5.18.4 - version: 5.18.4(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/icons-material@5.15.19)(@mui/material@5.15.19)(@rjsf/core@5.18.4)(@rjsf/utils@5.18.4)(react@18.3.1) + version: 5.18.4(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/icons-material@5.15.20)(@mui/material@5.15.19)(@rjsf/core@5.18.4)(@rjsf/utils@5.18.4)(react@18.3.1) '@rjsf/utils': specifier: ^5.18.4 version: 5.18.4(react@18.3.1) @@ -2023,8 +2026,8 @@ packages: resolution: {integrity: sha512-tCHSi/Tomez9ERynFhZRvFO6n9ATyrPs+2N80DMDzp6xDVirbBjEwhPcE+x7Lj+nwYw0SqFkOxyvMP0irnm55w==} dev: false - /@mui/icons-material@5.15.19(@mui/material@5.15.19)(@types/react@18.3.3)(react@18.3.1): - resolution: {integrity: sha512-RsEiRxA5azN9b8gI7JRqekkgvxQUlitoBOtZglflb8cUDyP12/cP4gRwhb44Ea1/zwwGGjAj66ZJpGHhKfibNA==} + /@mui/icons-material@5.15.20(@mui/material@5.15.19)(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-oGcKmCuHaYbAAoLN67WKSXtHmEgyWcJToT1uRtmPyxMj9N5uqwc/mRtEnst4Wj/eGr+zYH2FiZQ79v9k7kSk1Q==} engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 @@ -2512,7 +2515,7 @@ packages: react: 18.3.1 dev: false - /@rjsf/mui@5.18.4(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/icons-material@5.15.19)(@mui/material@5.15.19)(@rjsf/core@5.18.4)(@rjsf/utils@5.18.4)(react@18.3.1): + /@rjsf/mui@5.18.4(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/icons-material@5.15.20)(@mui/material@5.15.19)(@rjsf/core@5.18.4)(@rjsf/utils@5.18.4)(react@18.3.1): resolution: {integrity: sha512-SDKBcp/LOCdb8Rn4f4mBiEV8M+FCFQWK8KB4ymyzqjB5/2sVf5rw9u7o84SSXmdnQKt4SAztQf3Sd8tLhEzvPg==} engines: {node: '>=14'} peerDependencies: @@ -2526,7 +2529,7 @@ packages: dependencies: '@emotion/react': 11.11.4(@types/react@18.3.3)(react@18.3.1) '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.3.3)(react@18.3.1) - '@mui/icons-material': 5.15.19(@mui/material@5.15.19)(@types/react@18.3.3)(react@18.3.1) + '@mui/icons-material': 5.15.20(@mui/material@5.15.19)(@types/react@18.3.3)(react@18.3.1) '@mui/material': 5.15.19(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) '@rjsf/core': 5.18.4(@rjsf/utils@5.18.4)(react@18.3.1) '@rjsf/utils': 5.18.4(react@18.3.1) diff --git a/src/components/AddFieldModal.tsx b/src/components/AddFieldModal.tsx index 2ccd858..372a7a0 100644 --- a/src/components/AddFieldModal.tsx +++ b/src/components/AddFieldModal.tsx @@ -1,4 +1,4 @@ -import {Box, Button, Dialog, FormControl, InputLabel, MenuItem, Stack, TextField,} from "@mui/material"; +import {Box, Button, Dialog, FormControl, IconButton, InputLabel, MenuItem, Stack, TextField,} from "@mui/material"; import Form from "@rjsf/mui"; import validator from "@rjsf/validator-ajv8"; @@ -7,6 +7,7 @@ import {useSchema} from "../providers/SchemaProvider"; import {JsonSchemaType} from "../types"; import {JsonSchemaField} from "../fields/JsonSchemaField"; import Select from "@mui/material/Select"; +import {Add} from "@mui/icons-material"; const AddFieldModal = () => { const [open, setOpen] = React.useState(false); @@ -27,6 +28,8 @@ const AddFieldModal = () => { const handleSubmit = (formData) => { + console.log('🐕 sag formData', formData); // TODO: REMOVE ME ⚠️ + if (field) { field.setSchema(formData); dispatch({ @@ -54,7 +57,7 @@ const AddFieldModal = () => { return ( <> - + setOpen(true)}> setOpen(false)}>

{step !== 0 && }Adding Field

diff --git a/src/components/SchemaBuilder.tsx b/src/components/SchemaBuilder.tsx index 5772874..9571ece 100644 --- a/src/components/SchemaBuilder.tsx +++ b/src/components/SchemaBuilder.tsx @@ -1,24 +1,42 @@ import Form from "@rjsf/mui"; import validator from "@rjsf/validator-ajv8"; -import React from "react"; +import React, {useState} from "react"; import AddFieldModal from "./AddFieldModal"; -import {SchemaProvider, useSchema} from "../providers/SchemaProvider"; -import {JsonSchemaField} from "../fields/JsonSchemaField"; -import {FieldConfig} from "../types"; +import {useSchema} from "../providers/SchemaProvider"; +import {Button, CssBaseline, ButtonGroup, Tabs, Tab} from "@mui/material"; +import SchemaPreview from "./SchemaPreview"; const SchemaBuilder = () => { const {schema} = useSchema(); - + const [tab, setTab] = useState(0) + const TABS: Record<'BUILDER' | "SCHEMA" | "FORM_PREVIEW", number> = { + 'BUILDER': 0, "SCHEMA": 1, "FORM_PREVIEW": 2 + } + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTab(newValue); + }; return ( -
-

Form Preview:

-
-

Schema:

-
{JSON.stringify(schema, null, 2)}
-
- -
+ <> + +
+ + + + + + + {tab === TABS.BUILDER && ( + + )} + {tab === TABS.SCHEMA && ( +
{JSON.stringify(schema, null, 2)}
+ )} + {tab === TABS.FORM_PREVIEW && ( + + )} +
+ ); }; diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx new file mode 100644 index 0000000..96bbb46 --- /dev/null +++ b/src/components/SchemaPreview.tsx @@ -0,0 +1,129 @@ +import React from "react"; +import {RJSFSchema} from "@rjsf/utils"; +import AddFieldModal from "./AddFieldModal"; +import Numbers from '@mui/icons-material/Numbers'; +import { + Add, + DataArray, + DataObject, + Delete, + Edit, + ExpandLess, + ExpandMore, + TextSnippet, + ToggleOn +} from "@mui/icons-material"; +import {Box, Collapse, IconButton, ListItem, ListItemIcon, ListItemText, Tooltip, Typography} from "@mui/material"; +import {getSchemaFormatFromSchema} from "../utils"; +import {DataVisualizationType} from "../types"; + +type Props = { + schema: RJSFSchema; + data: unknown; +} + + +const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}) => ( + + + {icon && {icon}} + {schema?.title && } + + + + {collapse !== undefined && {collapse ? : + }} + +) + + +const SchemaPreview = ({schema, data}: Props) => { + const FormPreview = getSchemaFormatFromSchema(schema, SchemaPreview) + return ( +
+ +
+ ) +}; + +SchemaPreview.String = function String({schema, data}: DataVisualizationType) { + return ( +
+ {renderHeader({schema, icon: })} +
+ ); +}; + +SchemaPreview.Number = function Number({schema, data}: DataVisualizationType) { + return ( +
+ {renderHeader({schema, icon: })} +
+ ); +}; + +SchemaPreview.Boolean = function BooleanVisualization({schema, data}: DataVisualizationType) { + return ( +
+ {renderHeader({schema, icon: })} +
+ ); +}; + +SchemaPreview.Object = function ObjectVisualization({schema, data}: DataVisualizationType) { + const properties = Object.keys(schema?.properties || {}) + + const [open, setOpen] = React.useState(true); + + const handleCollapse = () => { + setOpen(!open); + }; + + return ( +
+ {renderHeader({schema, icon: , collapse: open, onCollapse: handleCollapse})} + + + +

Properties

+ +
+ {properties.length > 0 ? properties.map((property) => ( + + )) : ( + Click on button to + add + properties + )} +
+
+ ); +}; + +SchemaPreview.Array = function ArrayVisualization({schema, data}: DataVisualizationType) { + return ( + <> + {renderHeader({schema, icon: })} + + {data?.map((item, index) => ( +
+ +
+ ))} +
+ + ); +}; + +SchemaPreview.Unknown = function ArrayVisualization() { + return <>; +}; + +export default SchemaPreview; diff --git a/src/fields/containers/ArrayField.ts b/src/fields/containers/ArrayField.ts index a0ba76e..f33dbef 100644 --- a/src/fields/containers/ArrayField.ts +++ b/src/fields/containers/ArrayField.ts @@ -1,9 +1,10 @@ import {JsonSchemaField} from "../JsonSchemaField"; import {produce} from "immer"; -import {JsonSchema, SchemaAnnotation} from "../../types"; +import {JsonSchema, JsonSchemaType, SchemaAnnotation} from "../../types"; export type ArrayFieldType = SchemaAnnotation & { items?: JsonSchema | JsonSchema[]; + itemsType?: JsonSchemaType; prefixItems?: object; // TODO: fix type unevaluatedItems?: boolean | object; maxItems?: number; @@ -13,6 +14,8 @@ export type ArrayFieldType = SchemaAnnotation & { export class ArrayField extends JsonSchemaField { protected items?: JsonSchema | JsonSchema[]; + protected itemsType?: JsonSchemaType; + protected maxItems?: number; protected minItems?: number; @@ -21,7 +24,7 @@ export class ArrayField extends JsonSchemaField { protected unevaluatedItems?: boolean | object; - constructor(name: string) { + constructor(name: string) { super(name); this.type = "array"; } @@ -31,6 +34,12 @@ export class ArrayField extends JsonSchemaField { return this; } + + setItemsType(itemsType: JsonSchemaType): this { + this.itemsType = itemsType; + return this; + } + setMaxItems(maxItems: number): this { this.maxItems = maxItems; return this; @@ -51,21 +60,27 @@ export class ArrayField extends JsonSchemaField { return this; } - setSchema(schema: ArrayFieldType) { + setSchema(schema: ArrayFieldType & { itemsType?: JsonSchema }) { super.setSchema(schema as SchemaAnnotation); - if (schema.items) this.setItems(schema.items); + if (schema.itemsType) this.setItemsType(schema.itemsType); + if (schema.items) this.setItems({...schema.items, ...(this.itemsType && {type: this.itemsType})}); if (schema.maxItems) this.setMaxItems(schema.maxItems); if (schema.minItems) this.setMinItems(schema.minItems); if (schema.prefixItems) this.setPrefixItems(schema.prefixItems); if (schema.unevaluatedItems) this.setUnevaluatedItems(schema.unevaluatedItems); + + console.log('🐕 sag this.items', this.items); // TODO: REMOVE ME ⚠️ + console.log('🐕 sag this.itemsType', this.itemsType); // TODO: REMOVE ME ⚠️ + } getBuilderSchema(): JsonSchema { const arraySchema: Record = { - items: { - title: 'items', - type: 'object', + itemsType: { + title: 'Items Type', + type: 'string', + enum: ['string', 'number', 'boolean', 'integer', 'array', 'object'] }, prefixItems: { title: 'prefixItems', @@ -96,7 +111,10 @@ export class ArrayField extends JsonSchemaField { public getSchema(): JsonSchema { return { ...super.getSchema(), - items: this.items, + items: { + type: this.itemsType, + ...this.items, + }, prefixItems: this.prefixItems, unevaluatedItems: this.unevaluatedItems, minItems: this.minItems, diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index 794f441..bca88e3 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -26,12 +26,87 @@ interface SchemaAction { payload: { name: string; schema?: JsonSchema; value?: any }; } +const sampleSchema = { + "title": "Example Schema", + "description": "A rich JSON schema example without dependencies and no nested objects.", + "type": "object", + "properties": { + "id": { + "title": "Identifier", + "description": "A unique identifier for the item.", + "type": "string", + "pattern": "^[a-zA-Z0-9-]+$" + }, + "name": { + "title": "Name", + "description": "The name of the item.", + "type": "string", + "minLength": 1 + }, + "price": { + "title": "Price", + "description": "The price of the item.", + "type": "number", + "minimum": 0 + }, + "tags": { + "title": "Tags", + "description": "Tags associated with the item.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "length": { + "title": "Length", + "description": "The length of the item.", + "type": "number", + "minimum": 0 + }, + "width": { + "title": "Width", + "description": "The width of the item.", + "type": "number", + "minimum": 0 + }, + "height": { + "title": "Height", + "description": "The height of the item.", + "type": "number", + "minimum": 0 + }, + "latitude": { + "title": "Latitude", + "description": "Latitude of the warehouse location.", + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "title": "Longitude", + "description": "Longitude of the warehouse location.", + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "inStock": { + "title": "In Stock", + "description": "Indicates if the item is in stock.", + "type": "boolean" + } + }, + "required": ["id", "name", "price"], + "additionalProperties": false +} + export const SchemaContext = createContext<{ schema: JsonSchema; dispatch: Dispatch; fields: FieldConfig[]; }>({ - schema: new JsonSchemaBuilder().setType("object").build(), + schema: sampleSchema, + // schema: new JsonSchemaBuilder().setType("object").build(), dispatch: () => null, fields: [] }); @@ -121,7 +196,8 @@ type Props = { export const SchemaProvider = ({ children, extraFields }: Props) => { const [schema, dispatch] = useReducer( schemaReducer, - new JsonSchemaBuilder().setType("object").build() + sampleSchema + // new JsonSchemaBuilder().setType("object").build() ); return ( diff --git a/src/types.ts b/src/types.ts index 1e23810..e6d1c58 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ import {ArrayFieldType} from "./fields/containers/ArrayField"; import {IntegerFieldType} from "./fields/primitives/IntegerField"; import {BooleanFieldType} from "./fields/primitives/BooleanField"; import {JsonSchemaField} from "./fields/JsonSchemaField"; +import {RJSFSchema} from "@rjsf/utils"; export type BuiltInFormats = | "date-time" @@ -65,4 +66,12 @@ export type FieldConfig = { title: string; description: string; Class: JsonSchemaField; -} \ No newline at end of file +} + + +// Visualization + +export type DataVisualizationType = { + data: unknown; + schema: RJSFSchema; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..8a939a6 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,40 @@ +import {RJSFSchema} from "@rjsf/utils"; +import {DataVisualizationType} from "./types"; + +export const getSchemaFormatFromSchema = ( + schema: RJSFSchema, + SchemaFormat: { + ({schema, data}: DataVisualizationType): JSX.Element; + String({schema, data}: DataVisualizationType): JSX.Element; + Boolean({schema, data}: DataVisualizationType): JSX.Element; + Image({schema, data}: DataVisualizationType): JSX.Element; + Video({schema, data}: DataVisualizationType): JSX.Element; + Object({schema, data}: DataVisualizationType): JSX.Element; + Array({schema, data}: DataVisualizationType): JSX.Element; + Url({schema, data}: DataVisualizationType): JSX.Element; + RichText({schema, data}: DataVisualizationType): JSX.Element; + Map({schema, data}: DataVisualizationType): JSX.Element; + Date({schema, data}: DataVisualizationType): JSX.Element; + DateTime({schema, data}: DataVisualizationType): JSX.Element; + Enum({schema, data}: DataVisualizationType): JSX.Element; + Number({schema, data}: DataVisualizationType): JSX.Element; + Color({schema, data}: DataVisualizationType): JSX.Element; + Unknown({schema, data}: DataVisualizationType): JSX.Element; + }, +) => { + if (schema?.type === 'boolean') return SchemaFormat.Boolean; + if (schema?.type === 'string' && schema?.format === 'image-url') return SchemaFormat.Image; + if (schema?.type === 'string' && schema?.format === 'video-url') return SchemaFormat.Video; + if (schema?.type === 'string' && schema?.format === 'uri') return SchemaFormat.Url; + if (schema?.type === 'string' && schema?.format === 'advance') return SchemaFormat.RichText; + if (schema?.type === 'string' && schema?.ui?.widget === 'color') return SchemaFormat.Color; + if (schema?.format === 'date') return SchemaFormat.Date; + if (schema?.format === 'date-time') return SchemaFormat.DateTime; + if (schema?.type === 'string' && schema?.enum?.length > 0) return SchemaFormat.Enum; + if (schema?.type === 'string') return SchemaFormat.String; + if (schema?.type === 'number' || schema?.type === 'integer') return SchemaFormat.Number; + if (schema?.type === 'object' && schema?.format === 'map') return SchemaFormat.Map; + if (schema?.type === 'object') return SchemaFormat.Object; + if (schema?.type === 'array') return SchemaFormat.Array; + return SchemaFormat.Unknown; +}; \ No newline at end of file From b087f5885b696979dd87e205aedf4a69000a8c7a Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 11:30:49 +0330 Subject: [PATCH 2/8] feat: implement delete logic --- src/builder/JsonSchemaBuilder.ts | 21 ++ src/components/SchemaPreview.tsx | 76 ++++--- src/providers/SchemaProvider.tsx | 357 +++++++++++++++---------------- 3 files changed, 247 insertions(+), 207 deletions(-) diff --git a/src/builder/JsonSchemaBuilder.ts b/src/builder/JsonSchemaBuilder.ts index ccd1a7d..fdf2714 100644 --- a/src/builder/JsonSchemaBuilder.ts +++ b/src/builder/JsonSchemaBuilder.ts @@ -157,6 +157,27 @@ export class JsonSchemaBuilder { return this; } + deleteProperty(name: string): JsonSchemaBuilder { + if (this.schema.properties && this.schema.properties[name]) { + delete this.schema.properties[name]; + } + return this; + } + + deleteRequired(name: string): JsonSchemaBuilder { + if (this.schema.required) { + this.schema.required = this.schema.required.filter((req: string) => req !== name); + } + return this; + } + + editProperty(name: string, propSchema: JsonSchema): JsonSchemaBuilder { + if (this.schema.properties && this.schema.properties[name]) { + this.schema.properties[name] = propSchema; + } + return this; + } + build(): JsonSchema { return this.schema; } diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index 96bbb46..25c88bc 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -16,61 +16,75 @@ import { import {Box, Collapse, IconButton, ListItem, ListItemIcon, ListItemText, Tooltip, Typography} from "@mui/material"; import {getSchemaFormatFromSchema} from "../utils"; import {DataVisualizationType} from "../types"; +import {useSchema} from "../providers/SchemaProvider"; type Props = { schema: RJSFSchema; data: unknown; + name: string; } -const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}) => ( - - - {icon && {icon}} - {schema?.title && } - - +const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}: { + icon?: React.ReactNode, + schema: RJSFSchema, + onDelete?: () => void, + collapse?: boolean; + onCollapse: () => void +}) => ( + + {icon && {icon}} + {schema?.title && } + {onDelete && } {collapse !== undefined && {collapse ? : }} ) +const handleDelete = (dispatch, name) => { + dispatch({type: "DELETE_PROPERTY", payload: {name}}); + dispatch({type: "DELETE_REQUIRED", payload: {name}}); +} -const SchemaPreview = ({schema, data}: Props) => { +const SchemaPreview = ({schema, data, name}: Props) => { const FormPreview = getSchemaFormatFromSchema(schema, SchemaPreview) return (
- +
) }; -SchemaPreview.String = function String({schema, data}: DataVisualizationType) { +SchemaPreview.String = function String({schema, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: })} + {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; -SchemaPreview.Number = function Number({schema, data}: DataVisualizationType) { +SchemaPreview.Number = function Number({schema, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: })} + {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; -SchemaPreview.Boolean = function BooleanVisualization({schema, data}: DataVisualizationType) { +SchemaPreview.Boolean = function BooleanVisualization({schema, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: })} + {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; -SchemaPreview.Object = function ObjectVisualization({schema, data}: DataVisualizationType) { +SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); const properties = Object.keys(schema?.properties || {}) const [open, setOpen] = React.useState(true); @@ -83,7 +97,13 @@ SchemaPreview.Object = function ObjectVisualization({schema, data}: DataVisualiz
- {renderHeader({schema, icon: , collapse: open, onCollapse: handleCollapse})} + {renderHeader({ + schema, + icon: , + collapse: open, + onCollapse: handleCollapse, + onDelete: () => handleDelete(dispatch, name) + })} @@ -92,6 +112,7 @@ SchemaPreview.Object = function ObjectVisualization({schema, data}: DataVisualiz {properties.length > 0 ? properties.map((property) => ( @@ -105,19 +126,18 @@ SchemaPreview.Object = function ObjectVisualization({schema, data}: DataVisualiz ); }; -SchemaPreview.Array = function ArrayVisualization({schema, data}: DataVisualizationType) { +SchemaPreview.Array = function ArrayVisualization({schema, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); return ( <> - {renderHeader({schema, icon: })} - - {data?.map((item, index) => ( -
- -
- ))} -
+ {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {data?.map((item, index) => ( +
+ +
+ ))} ); }; diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index bca88e3..fceb757 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -1,5 +1,3 @@ -// SchemaProvider.tsx - import React, {createContext, Dispatch, ReactNode, useContext, useReducer} from "react"; import {JsonSchemaBuilder} from "../builder/JsonSchemaBuilder"; import {FieldConfig, JsonSchema} from "../types"; @@ -7,204 +5,205 @@ import {PROPERTIES} from "../constants"; interface SchemaAction { - type: "ADD_PROPERTY" | "UPDATE_PROPERTY" | "ADD_REQUIRED"; - payload: { name: string; schema?: JsonSchema; value?: any }; + type: "ADD_PROPERTY" | "UPDATE_PROPERTY" | "DELETE_PROPERTY" | "ADD_REQUIRED" | "DELETE_REQUIRED"; + payload: { name: string; schema?: JsonSchema; value?: any }; } const addAnnotations = (builder: JsonSchemaBuilder, state: JsonSchema) => { - builder.setType(state.type || "object"); - if (state.title) builder.setTitle(state.title); - if (state.description) builder.setDescription(state.description); - if (state.default) builder.setDefault(state.default); - if (state.readOnly) builder.setReadOnly(state.readOnly); - if (state.writeOnly) builder.setWriteOnly(state.writeOnly); + builder.setType(state.type || "object"); + if (state.title) builder.setTitle(state.title); + if (state.description) builder.setDescription(state.description); + if (state.default) builder.setDefault(state.default); + if (state.readOnly) builder.setReadOnly(state.readOnly); + if (state.writeOnly) builder.setWriteOnly(state.writeOnly); }; - -interface SchemaAction { - type: "ADD_PROPERTY" | "UPDATE_PROPERTY" | "ADD_REQUIRED"; - payload: { name: string; schema?: JsonSchema; value?: any }; -} - const sampleSchema = { - "title": "Example Schema", - "description": "A rich JSON schema example without dependencies and no nested objects.", - "type": "object", - "properties": { - "id": { - "title": "Identifier", - "description": "A unique identifier for the item.", - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "name": { - "title": "Name", - "description": "The name of the item.", - "type": "string", - "minLength": 1 - }, - "price": { - "title": "Price", - "description": "The price of the item.", - "type": "number", - "minimum": 0 - }, - "tags": { - "title": "Tags", - "description": "Tags associated with the item.", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "length": { - "title": "Length", - "description": "The length of the item.", - "type": "number", - "minimum": 0 - }, - "width": { - "title": "Width", - "description": "The width of the item.", - "type": "number", - "minimum": 0 - }, - "height": { - "title": "Height", - "description": "The height of the item.", - "type": "number", - "minimum": 0 + "title": "Example Schema", + "description": "A rich JSON schema example without dependencies and no nested objects.", + "type": "object", + "properties": { + "id": { + "title": "Identifier", + "description": "A unique identifier for the item.", + "type": "string", + "pattern": "^[a-zA-Z0-9-]+$" + }, + "name": { + "title": "Name", + "description": "The name of the item.", + "type": "string", + "minLength": 1 + }, + "price": { + "title": "Price", + "description": "The price of the item.", + "type": "number", + "minimum": 0 + }, + "tags": { + "title": "Tags", + "description": "Tags associated with the item.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "length": { + "title": "Length", + "description": "The length of the item.", + "type": "number", + "minimum": 0 + }, + "width": { + "title": "Width", + "description": "The width of the item.", + "type": "number", + "minimum": 0 + }, + "height": { + "title": "Height", + "description": "The height of the item.", + "type": "number", + "minimum": 0 + }, + "latitude": { + "title": "Latitude", + "description": "Latitude of the warehouse location.", + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "title": "Longitude", + "description": "Longitude of the warehouse location.", + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "inStock": { + "title": "In Stock", + "description": "Indicates if the item is in stock.", + "type": "boolean" + } }, - "latitude": { - "title": "Latitude", - "description": "Latitude of the warehouse location.", - "type": "number", - "minimum": -90, - "maximum": 90 - }, - "longitude": { - "title": "Longitude", - "description": "Longitude of the warehouse location.", - "type": "number", - "minimum": -180, - "maximum": 180 - }, - "inStock": { - "title": "In Stock", - "description": "Indicates if the item is in stock.", - "type": "boolean" - } - }, - "required": ["id", "name", "price"], - "additionalProperties": false -} + "required": ["id", "name", "price"], + "additionalProperties": false +}; export const SchemaContext = createContext<{ - schema: JsonSchema; - dispatch: Dispatch; - fields: FieldConfig[]; + schema: JsonSchema; + dispatch: Dispatch; + fields: FieldConfig[]; }>({ - schema: sampleSchema, - // schema: new JsonSchemaBuilder().setType("object").build(), - dispatch: () => null, - fields: [] + schema: sampleSchema, + // schema: new JsonSchemaBuilder().setType("object").build(), + dispatch: () => null, + fields: [] }); const addSpecificProperties = ( - builder: JsonSchemaBuilder, - state: JsonSchema + builder: JsonSchemaBuilder, + state: JsonSchema ) => { - switch (state.type) { - case "string": - if (state.maxLength) builder.setMaxLength(state.maxLength); - if (state.minLength) builder.setMinLength(state.minLength); - if (state.pattern) builder.setPattern(state.pattern); - if (state.format) builder.setFormat(state.format); - if (state.contentEncoding) - builder.setContentEncoding(state.contentEncoding); - if (state.contentMediaType) - builder.setContentMediaType(state.contentMediaType); - break; - case "number": - if (state.multipleOf) builder.setMultipleOf(state.multipleOf); - if (state.maximum) builder.setMaximum(state.maximum); - if (state.minimum) builder.setMinimum(state.minimum); - if (state.exclusiveMaximum) - builder.setExclusiveMaximum(state.exclusiveMaximum); - if (state.exclusiveMinimum) - builder.setExclusiveMinimum(state.exclusiveMinimum); - break; - case "object": - if (state.properties) { - Object.keys(state.properties).forEach((key) => { - if (state.properties?.[key]) - builder.addProperty(key, state.properties[key]); - }); - } - if (state.required) { - builder.addRequired(...state.required); - } - if (state.patternProperties) - builder.setPatternProperties(state.patternProperties); - if (state.additionalProperties) - builder.setAdditionalProperties(state.additionalProperties); - - break; - case "array": - if (state.items) builder.setItems(state.items); - if (state.prefixItems) builder.setPrefixItems(state.prefixItems); - if (state.unevaluatedItems) - builder.setUnevaluatedItems(state.unevaluatedItems); - if (state.maxItems) builder.setMaxItems(state.maxItems); - if (state.minItems) builder.setMinItems(state.minItems); - break; - default: - break; - } + switch (state.type) { + case "string": + if (state.maxLength) builder.setMaxLength(state.maxLength); + if (state.minLength) builder.setMinLength(state.minLength); + if (state.pattern) builder.setPattern(state.pattern); + if (state.format) builder.setFormat(state.format); + if (state.contentEncoding) + builder.setContentEncoding(state.contentEncoding); + if (state.contentMediaType) + builder.setContentMediaType(state.contentMediaType); + break; + case "number": + if (state.multipleOf) builder.setMultipleOf(state.multipleOf); + if (state.maximum) builder.setMaximum(state.maximum); + if (state.minimum) builder.setMinimum(state.minimum); + if (state.exclusiveMaximum) + builder.setExclusiveMaximum(state.exclusiveMaximum); + if (state.exclusiveMinimum) + builder.setExclusiveMinimum(state.exclusiveMinimum); + break; + case "object": + if (state.properties) { + Object.keys(state.properties).forEach((key) => { + if (state.properties?.[key]) + builder.addProperty(key, state.properties[key]); + }); + } + if (state.required) { + builder.addRequired(...state.required); + } + if (state.patternProperties) + builder.setPatternProperties(state.patternProperties); + if (state.additionalProperties) + builder.setAdditionalProperties(state.additionalProperties); + + break; + case "array": + if (state.items) builder.setItems(state.items); + if (state.prefixItems) builder.setPrefixItems(state.prefixItems); + if (state.unevaluatedItems) + builder.setUnevaluatedItems(state.unevaluatedItems); + if (state.maxItems) builder.setMaxItems(state.maxItems); + if (state.minItems) builder.setMinItems(state.minItems); + break; + default: + break; + } }; const schemaReducer = (state: JsonSchema, action: SchemaAction): JsonSchema => { - const builder = new JsonSchemaBuilder(); - - addAnnotations(builder, state); - - addSpecificProperties(builder, state); - - if (state.enum) builder.setEnum(state.enum); - if (state.enumNames) builder.setEnumNames(state.enumNames); - - switch (action.type) { - case "ADD_PROPERTY": - builder.addProperty(action.payload.name, action.payload.schema!); - break; - case "ADD_REQUIRED": - builder.addRequired(action.payload.name); - break; - default: - return state; - } - - return builder.build(); + const builder = new JsonSchemaBuilder(); + + addAnnotations(builder, state); + addSpecificProperties(builder, state); + + if (state.enum) builder.setEnum(state.enum); + if (state.enumNames) builder.setEnumNames(state.enumNames); + + switch (action.type) { + case "ADD_PROPERTY": + builder.addProperty(action.payload.name, action.payload.schema!); + break; + case "UPDATE_PROPERTY": + builder.editProperty(action.payload.name, action.payload.schema!); + break; + case "DELETE_PROPERTY": + builder.deleteProperty(action.payload.name); + break; + case "ADD_REQUIRED": + builder.addRequired(action.payload.name); + break; + case "DELETE_REQUIRED": + builder.deleteRequired(action.payload.name); + break; + default: + return state; + } + + return builder.build(); }; type Props = { - extraFields: FieldConfig[]; - children: ReactNode; + extraFields: FieldConfig[]; + children: ReactNode; +}; -} -export const SchemaProvider = ({ children, extraFields }: Props) => { - const [schema, dispatch] = useReducer( - schemaReducer, - sampleSchema - // new JsonSchemaBuilder().setType("object").build() - ); - - return ( - - {children} - - ); +export const SchemaProvider = ({children, extraFields}: Props) => { + const [schema, dispatch] = useReducer( + schemaReducer, + sampleSchema + ); + + return ( + + {children} + + ); }; export const useSchema = () => useContext(SchemaContext); From 53bf5b263d91d096d6c80e9f1987ae89ce6f0287 Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 12:41:06 +0330 Subject: [PATCH 3/8] feat: enhance visualization --- src/components/SchemaPreview.tsx | 76 ++++++++++++++++++------ src/providers/SchemaProvider.tsx | 84 ++------------------------- src/stories/SchemaBuilder.stories.tsx | 84 +++++++++++++++++++++++++++ src/types.ts | 3 +- src/utils.ts | 16 ++--- 5 files changed, 158 insertions(+), 105 deletions(-) diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index 25c88bc..056c009 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useState} from "react"; import {RJSFSchema} from "@rjsf/utils"; import AddFieldModal from "./AddFieldModal"; import Numbers from '@mui/icons-material/Numbers'; @@ -13,10 +13,20 @@ import { TextSnippet, ToggleOn } from "@mui/icons-material"; -import {Box, Collapse, IconButton, ListItem, ListItemIcon, ListItemText, Tooltip, Typography} from "@mui/material"; +import { + Box, Button, Chip, + Collapse, + Dialog, + IconButton, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, + Typography +} from "@mui/material"; import {getSchemaFormatFromSchema} from "../utils"; import {DataVisualizationType} from "../types"; -import {useSchema} from "../providers/SchemaProvider"; +import {SchemaAction, useSchema} from "../providers/SchemaProvider"; type Props = { schema: RJSFSchema; @@ -25,28 +35,58 @@ type Props = { } +// TODO: refactor const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}: { icon?: React.ReactNode, schema: RJSFSchema, onDelete?: () => void, collapse?: boolean; - onCollapse: () => void -}) => ( - - {icon && {icon}} - {schema?.title && } - {onDelete && } - - {collapse !== undefined && {collapse ? : - }} - -) - -const handleDelete = (dispatch, name) => { + onCollapse?: () => void +}) => { + const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); + + return ( + <> + setShowDeleteConfirmationModal(false)}> + + Are you sure you want to delete this field? + + + + + + + {schema?.title} + {schema?.description && ( + {schema?.description} + )} + + )} + /> + {onDelete && setShowDeleteConfirmationModal(true)}>} + + {collapse !== undefined && {collapse ? : + }} + + + ) +} + +const handleDelete = (dispatch: React.Dispatch, name: string) => { dispatch({type: "DELETE_PROPERTY", payload: {name}}); dispatch({type: "DELETE_REQUIRED", payload: {name}}); } +const handleEdit = (dispatch: React.Dispatch, name: string, schema: RJSFSchema) => { + dispatch({type: "UPDATE_PROPERTY", payload: {name, schema}}); +} + const SchemaPreview = ({schema, data, name}: Props) => { const FormPreview = getSchemaFormatFromSchema(schema, SchemaPreview) return ( @@ -65,7 +105,7 @@ SchemaPreview.String = function String({schema, data, name}: DataVisualizationTy ); }; -SchemaPreview.Number = function Number({schema, data, name}: DataVisualizationType) { +SchemaPreview.Number = function Number({schema, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
@@ -74,7 +114,7 @@ SchemaPreview.Number = function Number({schema, data, name}: DataVisualizationTy ); }; -SchemaPreview.Boolean = function BooleanVisualization({schema, data, name}: DataVisualizationType) { +SchemaPreview.Boolean = function BooleanVisualization({schema, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index fceb757..b01d40c 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -2,9 +2,10 @@ import React, {createContext, Dispatch, ReactNode, useContext, useReducer} from import {JsonSchemaBuilder} from "../builder/JsonSchemaBuilder"; import {FieldConfig, JsonSchema} from "../types"; import {PROPERTIES} from "../constants"; +import {RJSFSchema} from "@rjsf/utils"; -interface SchemaAction { +export interface SchemaAction { type: "ADD_PROPERTY" | "UPDATE_PROPERTY" | "DELETE_PROPERTY" | "ADD_REQUIRED" | "DELETE_REQUIRED"; payload: { name: string; schema?: JsonSchema; value?: any }; } @@ -18,87 +19,13 @@ const addAnnotations = (builder: JsonSchemaBuilder, state: JsonSchema) => { if (state.writeOnly) builder.setWriteOnly(state.writeOnly); }; -const sampleSchema = { - "title": "Example Schema", - "description": "A rich JSON schema example without dependencies and no nested objects.", - "type": "object", - "properties": { - "id": { - "title": "Identifier", - "description": "A unique identifier for the item.", - "type": "string", - "pattern": "^[a-zA-Z0-9-]+$" - }, - "name": { - "title": "Name", - "description": "The name of the item.", - "type": "string", - "minLength": 1 - }, - "price": { - "title": "Price", - "description": "The price of the item.", - "type": "number", - "minimum": 0 - }, - "tags": { - "title": "Tags", - "description": "Tags associated with the item.", - "type": "array", - "items": { - "type": "string" - }, - "uniqueItems": true - }, - "length": { - "title": "Length", - "description": "The length of the item.", - "type": "number", - "minimum": 0 - }, - "width": { - "title": "Width", - "description": "The width of the item.", - "type": "number", - "minimum": 0 - }, - "height": { - "title": "Height", - "description": "The height of the item.", - "type": "number", - "minimum": 0 - }, - "latitude": { - "title": "Latitude", - "description": "Latitude of the warehouse location.", - "type": "number", - "minimum": -90, - "maximum": 90 - }, - "longitude": { - "title": "Longitude", - "description": "Longitude of the warehouse location.", - "type": "number", - "minimum": -180, - "maximum": 180 - }, - "inStock": { - "title": "In Stock", - "description": "Indicates if the item is in stock.", - "type": "boolean" - } - }, - "required": ["id", "name", "price"], - "additionalProperties": false -}; export const SchemaContext = createContext<{ schema: JsonSchema; dispatch: Dispatch; fields: FieldConfig[]; }>({ - schema: sampleSchema, - // schema: new JsonSchemaBuilder().setType("object").build(), + schema: new JsonSchemaBuilder().setType("object").build(), dispatch: () => null, fields: [] }); @@ -191,12 +118,13 @@ const schemaReducer = (state: JsonSchema, action: SchemaAction): JsonSchema => { type Props = { extraFields: FieldConfig[]; children: ReactNode; + value?: RJSFSchema; }; -export const SchemaProvider = ({children, extraFields}: Props) => { +export const SchemaProvider = ({children, extraFields, value}: Props) => { const [schema, dispatch] = useReducer( schemaReducer, - sampleSchema + value || new JsonSchemaBuilder().setType("object").build() ); return ( diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index 1722fe7..2803c36 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -4,6 +4,80 @@ import SchemaBuilder from '../components/SchemaBuilder'; import {STRING_WIDGETS} from "../constants"; import {SchemaProvider} from "../providers/SchemaProvider"; +const sampleSchema = { + "title": "Example Schema", + "description": "A rich JSON schema example without dependencies and no nested objects.", + "type": "object", + "properties": { + "id": { + "title": "Identifier", + "description": "A unique identifier for the item.", + "type": "string", + "pattern": "^[a-zA-Z0-9-]+$" + }, + "name": { + "title": "Name", + "description": "The name of the item.", + "type": "string", + "minLength": 1 + }, + "price": { + "title": "Price", + "description": "The price of the item.", + "type": "number", + "minimum": 0 + }, + "tags": { + "title": "Tags", + "description": "Tags associated with the item.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "length": { + "title": "Length", + "description": "The length of the item.", + "type": "number", + "minimum": 0 + }, + "width": { + "title": "Width", + "description": "The width of the item.", + "type": "number", + "minimum": 0 + }, + "height": { + "title": "Height", + "description": "The height of the item.", + "type": "number", + "minimum": 0 + }, + "latitude": { + "title": "Latitude", + "description": "Latitude of the warehouse location.", + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "title": "Longitude", + "description": "Longitude of the warehouse location.", + "type": "number", + "minimum": -180, + "maximum": 180 + }, + "inStock": { + "title": "In Stock", + "description": "Indicates if the item is in stock.", + "type": "boolean" + } + }, + "required": ["id", "name", "price"], + "additionalProperties": false +}; + export default { title: 'SchemaBuilder', component: SchemaBuilder, @@ -24,3 +98,13 @@ Formats.args = { extraFields: [...STRING_WIDGETS] }; +const DefaultValueTemplate: Story = (args) => ( + + + +); + +export const WithDefaultValue = DefaultValueTemplate.bind({}); +WithDefaultValue.args = { + extraFields: [...STRING_WIDGETS] +}; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index e6d1c58..cf7cde2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,6 +72,7 @@ export type FieldConfig = { // Visualization export type DataVisualizationType = { - data: unknown; + data?: object; schema: RJSFSchema; + name?: string } diff --git a/src/utils.ts b/src/utils.ts index 8a939a6..cf280c1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,14 +23,14 @@ export const getSchemaFormatFromSchema = ( }, ) => { if (schema?.type === 'boolean') return SchemaFormat.Boolean; - if (schema?.type === 'string' && schema?.format === 'image-url') return SchemaFormat.Image; - if (schema?.type === 'string' && schema?.format === 'video-url') return SchemaFormat.Video; - if (schema?.type === 'string' && schema?.format === 'uri') return SchemaFormat.Url; - if (schema?.type === 'string' && schema?.format === 'advance') return SchemaFormat.RichText; - if (schema?.type === 'string' && schema?.ui?.widget === 'color') return SchemaFormat.Color; - if (schema?.format === 'date') return SchemaFormat.Date; - if (schema?.format === 'date-time') return SchemaFormat.DateTime; - if (schema?.type === 'string' && schema?.enum?.length > 0) return SchemaFormat.Enum; + // if (schema?.type === 'string' && schema?.format === 'image-url') return SchemaFormat.Image; + // if (schema?.type === 'string' && schema?.format === 'video-url') return SchemaFormat.Video; + // if (schema?.type === 'string' && schema?.format === 'uri') return SchemaFormat.Url; + // if (schema?.type === 'string' && schema?.format === 'advance') return SchemaFormat.RichText; + // if (schema?.type === 'string' && schema?.ui?.widget === 'color') return SchemaFormat.Color; + // if (schema?.format === 'date') return SchemaFormat.Date; + // if (schema?.format === 'date-time') return SchemaFormat.DateTime; + // if (schema?.type === 'string' && schema?.enum?.length > 0) return SchemaFormat.Enum; if (schema?.type === 'string') return SchemaFormat.String; if (schema?.type === 'number' || schema?.type === 'integer') return SchemaFormat.Number; if (schema?.type === 'object' && schema?.format === 'map') return SchemaFormat.Map; From e7d1eadb5d4a38940af79014f9f2a7166596888c Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 13:45:53 +0330 Subject: [PATCH 4/8] fix: add utils --- src/components/SchemaPreview.tsx | 26 +++++++++++++++++++------- src/utils.ts | 13 ++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index 056c009..ad114e6 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -24,9 +24,11 @@ import { Tooltip, Typography } from "@mui/material"; -import {getSchemaFormatFromSchema} from "../utils"; +import {getFieldId, getSchemaFormatFromSchema} from "../utils"; import {DataVisualizationType} from "../types"; import {SchemaAction, useSchema} from "../providers/SchemaProvider"; +import Form from "@rjsf/mui"; +import validator from "@rjsf/validator-ajv8"; type Props = { schema: RJSFSchema; @@ -36,9 +38,10 @@ type Props = { // TODO: refactor -const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}: { +const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { icon?: React.ReactNode, schema: RJSFSchema, + name: string, onDelete?: () => void, collapse?: boolean; onCollapse?: () => void @@ -47,6 +50,7 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}: { return ( <> + setShowDeleteConfirmationModal(false)}> Are you sure you want to delete this field? @@ -61,7 +65,14 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse}: { - {schema?.title} + {schema?.title} + {schema?.description && ( {schema?.description} )} @@ -100,7 +111,7 @@ SchemaPreview.String = function String({schema, data, name}: DataVisualizationTy const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; @@ -109,7 +120,7 @@ SchemaPreview.Number = function Number({schema, name}: DataVisualizationType) { const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; @@ -118,7 +129,7 @@ SchemaPreview.Boolean = function BooleanVisualization({schema, name}: DataVisual const {dispatch} = useSchema(); return (
- {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})}
); }; @@ -138,6 +149,7 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi style={{border: "#0005 solid 1px", padding: "20px", margin: "5px"}} > {renderHeader({ + name, schema, icon: , collapse: open, @@ -170,7 +182,7 @@ SchemaPreview.Array = function ArrayVisualization({schema, data, name}: DataVisu const {dispatch} = useSchema(); return ( <> - {renderHeader({schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name,schema, icon: , onDelete: () => handleDelete(dispatch, name)})} {data?.map((item, index) => (
{ + if (schema?.type === 'boolean') return 'BOOLEAN'; + if (schema?.format === 'date') return 'DATE'; + if (schema?.format === 'date-time') return 'DATE_TIME'; + if (schema?.format === 'time') return 'TIME'; + if (schema?.type === 'string') return 'STRING'; + 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 From 7b6e92cb2e2aa3dd4dd82597c5bf9ada435460bd Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 13:46:10 +0330 Subject: [PATCH 5/8] fix: add utils --- src/components/SchemaPreview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index ad114e6..d9800ab 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -50,7 +50,6 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { return ( <> - setShowDeleteConfirmationModal(false)}> Are you sure you want to delete this field? From 811edc271a125afacd1c8ba03ad6a83136fb60ae Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 14:32:22 +0330 Subject: [PATCH 6/8] fix: implement edit feature --- src/components/SchemaPreview.tsx | 80 +++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index d9800ab..9babc3e 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -16,7 +16,7 @@ import { import { Box, Button, Chip, Collapse, - Dialog, + Dialog, DialogActions, DialogContent, DialogTitle, IconButton, ListItem, ListItemIcon, @@ -47,18 +47,37 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { onCollapse?: () => void }) => { const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); - + const [showEditModal, setShowEditModal] = useState(false); + const {fields, dispatch} = useSchema(); + const SelectedFieldClass = fields.find(field => field.id === getFieldId(schema))?.Class + console.log('🐕 sag selectedField', SelectedFieldClass); // TODO: REMOVE ME ⚠️ + + let field; + if (SelectedFieldClass) { + field = new SelectedFieldClass(name) + } return ( <> + setShowEditModal(false)}> + Edit {name} Field + + { + console.log('🐕 sag formData', formData); // TODO: REMOVE ME ⚠️ + dispatch({ type: "UPDATE_PROPERTY", payload: { name, schema: formData }}) + setShowEditModal(false); + }} schema={field?.getBuilderSchema()} formData={schema} validator={validator}/> + + setShowDeleteConfirmationModal(false)}> - - Are you sure you want to delete this field? - - + - + {onDelete && setShowDeleteConfirmationModal(true)}>} - - {collapse !== undefined && {collapse ? : - }} + {true && setShowEditModal(true)}>} + ) @@ -155,24 +174,31 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi onCollapse: handleCollapse, onDelete: () => handleDelete(dispatch, name) })} - - - -

Properties

+
+ + Properties + {open !== undefined && + {!open ? : + }} - {properties.length > 0 ? properties.map((property) => ( - - )) : ( - Click on button to - add - properties - )} - + + {properties.length > 0 ? properties.map((property) => ( + + )) : ( + + Click on button to add properties + + )} + +
); }; @@ -181,7 +207,7 @@ SchemaPreview.Array = function ArrayVisualization({schema, data, name}: DataVisu const {dispatch} = useSchema(); return ( <> - {renderHeader({name,schema, icon: , onDelete: () => handleDelete(dispatch, name)})} + {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} {data?.map((item, index) => (
Date: Tue, 18 Jun 2024 15:32:57 +0330 Subject: [PATCH 7/8] fix: update storeis --- src/components/SchemaPreview.tsx | 44 ++++++++++----------- src/stories/SchemaBuilder.stories.tsx | 56 ++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index 9babc3e..6133dfd 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -14,7 +14,7 @@ import { ToggleOn } from "@mui/icons-material"; import { - Box, Button, Chip, + Box, Button, Card, Chip, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, @@ -38,7 +38,7 @@ type Props = { // TODO: refactor -const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { +const renderHeader = ({icon, schema, onDelete, name}: { icon?: React.ReactNode, schema: RJSFSchema, name: string, @@ -61,9 +61,8 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { setShowEditModal(false)}> Edit {name} Field - { - console.log('🐕 sag formData', formData); // TODO: REMOVE ME ⚠️ - dispatch({ type: "UPDATE_PROPERTY", payload: { name, schema: formData }}) + { + dispatch({type: "UPDATE_PROPERTY", payload: {name, schema: formData}}) setShowEditModal(false); }} schema={field?.getBuilderSchema()} formData={schema} validator={validator}/> @@ -99,8 +98,12 @@ const renderHeader = ({icon, schema, onDelete, collapse, onCollapse, name}: { /> {onDelete && setShowDeleteConfirmationModal(true)}>} - {true && setShowEditModal(true)}>} + setShowEditModal(true)} + > + + @@ -163,9 +166,7 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi }; return ( -
+ {renderHeader({ name, schema, @@ -174,9 +175,7 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi onCollapse: handleCollapse, onDelete: () => handleDelete(dispatch, name) })} -
+ Properties @@ -186,11 +185,10 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi }} - {properties.length > 0 ? properties.map((property) => ( + {properties?.length > 0 ? properties?.map((property) => ( )) : ( @@ -198,8 +196,8 @@ SchemaPreview.Object = function ObjectVisualization({schema, data, name}: DataVi )} -
-
+ + ); }; @@ -207,14 +205,12 @@ SchemaPreview.Array = function ArrayVisualization({schema, data, name}: DataVisu const {dispatch} = useSchema(); return ( <> + {renderHeader({name, schema, icon: , onDelete: () => handleDelete(dispatch, name)})} - {data?.map((item, index) => ( -
- -
- ))} + +
); }; diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index 2803c36..4308b67 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import { Story, Meta } from '@storybook/react'; +import {Story, Meta} from '@storybook/react'; import SchemaBuilder from '../components/SchemaBuilder'; import {STRING_WIDGETS} from "../constants"; import {SchemaProvider} from "../providers/SchemaProvider"; +import {createTheme, ThemeProvider} from "@mui/material"; const sampleSchema = { "title": "Example Schema", @@ -32,7 +33,27 @@ const sampleSchema = { "description": "Tags associated with the item.", "type": "array", "items": { - "type": "string" + "type": "string", + 'title': 'Tag Name' + }, + "uniqueItems": true + }, + "faq": { + "title": "FAQ", + "type": "array", + "items": { + "type": "object", + title: 'List of Questions', + properties: { + question: { + title: 'question', + type: 'string', + }, + answer: { + title: 'answer', + type: 'string', + } + } }, "uniqueItems": true }, @@ -79,13 +100,13 @@ const sampleSchema = { }; export default { - title: 'SchemaBuilder', - component: SchemaBuilder, + title: 'SchemaBuilder', + component: SchemaBuilder, } as Meta; const Template: Story = (args) => ( - + ); @@ -95,7 +116,7 @@ Primitives.args = {}; export const Formats = Template.bind({}); Formats.args = { - extraFields: [...STRING_WIDGETS] + extraFields: [...STRING_WIDGETS] }; const DefaultValueTemplate: Story = (args) => ( @@ -107,4 +128,27 @@ const DefaultValueTemplate: Story = (args) => ( export const WithDefaultValue = DefaultValueTemplate.bind({}); WithDefaultValue.args = { extraFields: [...STRING_WIDGETS] +}; +const ThemedTemplate: Story = (args) => { + const theme = createTheme(args.theme) + return ( + + + + + + ) +}; + +export const Themed = ThemedTemplate.bind({}); +Themed.args = { + extraFields: [...STRING_WIDGETS], + theme: { + palette: { + primary: { + main: '#ff5722' + }, + mode: 'dark' + } + } }; \ No newline at end of file From 1517d31d8e194ad7b04bc9960d2ce9902e149a83 Mon Sep 17 00:00:00 2001 From: Amirhossein Alibakhshi Date: Tue, 18 Jun 2024 15:34:30 +0330 Subject: [PATCH 8/8] fix: remove unused code --- src/components/AddFieldModal.tsx | 2 -- src/components/SchemaPreview.tsx | 1 - src/fields/containers/ArrayField.ts | 3 --- 3 files changed, 6 deletions(-) diff --git a/src/components/AddFieldModal.tsx b/src/components/AddFieldModal.tsx index 372a7a0..d65bfa5 100644 --- a/src/components/AddFieldModal.tsx +++ b/src/components/AddFieldModal.tsx @@ -28,8 +28,6 @@ const AddFieldModal = () => { const handleSubmit = (formData) => { - console.log('🐕 sag formData', formData); // TODO: REMOVE ME ⚠️ - if (field) { field.setSchema(formData); dispatch({ diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index 6133dfd..36fe94b 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -50,7 +50,6 @@ const renderHeader = ({icon, schema, onDelete, name}: { const [showEditModal, setShowEditModal] = useState(false); const {fields, dispatch} = useSchema(); const SelectedFieldClass = fields.find(field => field.id === getFieldId(schema))?.Class - console.log('🐕 sag selectedField', SelectedFieldClass); // TODO: REMOVE ME ⚠️ let field; if (SelectedFieldClass) { diff --git a/src/fields/containers/ArrayField.ts b/src/fields/containers/ArrayField.ts index f33dbef..cbc0242 100644 --- a/src/fields/containers/ArrayField.ts +++ b/src/fields/containers/ArrayField.ts @@ -68,9 +68,6 @@ export class ArrayField extends JsonSchemaField { if (schema.minItems) this.setMinItems(schema.minItems); if (schema.prefixItems) this.setPrefixItems(schema.prefixItems); if (schema.unevaluatedItems) this.setUnevaluatedItems(schema.unevaluatedItems); - - console.log('🐕 sag this.items', this.items); // TODO: REMOVE ME ⚠️ - console.log('🐕 sag this.itemsType', this.itemsType); // TODO: REMOVE ME ⚠️ }