diff --git a/src/components/AddFieldModal.tsx b/src/components/AddFieldModal.tsx index 9d46d37..3cff384 100644 --- a/src/components/AddFieldModal.tsx +++ b/src/components/AddFieldModal.tsx @@ -1,4 +1,15 @@ -import {Box, Button, Dialog, FormControl, IconButton, InputLabel, MenuItem, Stack, TextField,} from "@mui/material"; +import { + Box, + Button, + Dialog, + FormControl, + IconButton, + InputLabel, + MenuItem, + Stack, + TextField, + Tooltip, +} from "@mui/material"; import Form from "@rjsf/mui"; import validator from "@rjsf/validator-ajv8"; @@ -61,7 +72,7 @@ const AddFieldModal = ({ parentPath }) => { return ( <> - setOpen(true)}> + setOpen(true)}> setOpen(false)}>

{step !== 0 && }Adding Field

diff --git a/src/components/FieldPreview.tsx b/src/components/FieldPreview.tsx index 7148571..1026a1a 100644 --- a/src/components/FieldPreview.tsx +++ b/src/components/FieldPreview.tsx @@ -76,6 +76,15 @@ FieldPreview.String = function String({schema, data, name}: DataVisualizationTyp ); }; +FieldPreview.Enum = function Enum({schema, data, name}: DataVisualizationType) { + return ( + + {schema?.title} + {data || '-'} + + ); +}; + FieldPreview.Number = function Number({schema, name, data}: DataVisualizationType) { return ( diff --git a/src/components/SchemaPreview.tsx b/src/components/SchemaPreview.tsx index f6f43f1..7e8a47e 100644 --- a/src/components/SchemaPreview.tsx +++ b/src/components/SchemaPreview.tsx @@ -3,7 +3,7 @@ import {RJSFSchema} from "@rjsf/utils"; import AddFieldModal from "./AddFieldModal"; import Numbers from '@mui/icons-material/Numbers'; import { - Add, + Add, Checklist, DataArray, DataObject, Delete, @@ -20,9 +20,9 @@ import { IconButton, ListItem, ListItemIcon, - ListItemText, + ListItemText, Paper, Stack, Tooltip, - Typography + Typography, useTheme } from "@mui/material"; import {generatePath, getFieldId, getSchemaFormatFromSchema} from "../utils"; import {DataVisualizationType} from "../types"; @@ -39,10 +39,12 @@ type Props = { } // TODO: refactor -const renderHeader = ({icon, schema, onDelete, name, path}: { +const renderHeader = ({icon, schema, onDelete, name, path, description}: { icon?: React.ReactNode, schema: RJSFSchema, - name: string, + description?: React.ReactNode, + path: string, + name?: string, onDelete?: () => void, collapse?: boolean; onCollapse?: () => void @@ -107,8 +109,8 @@ const renderHeader = ({icon, schema, onDelete, name, path}: { label={`${schema?.type}${schema?.format ? `: ${schema?.format}` : ''}`} /> - {schema?.description && ( - {schema?.description} + {description && ( + {description} )} )} @@ -146,36 +148,73 @@ const handleEdit = (dispatch: React.Dispatch, name: string, schema const SchemaPreview = ({schema, data, name, path}: Props) => { const FormPreview = getSchemaFormatFromSchema(schema, SchemaPreview) return ( -
-
) }; SchemaPreview.String = function String({schema, path, data, name}: DataVisualizationType) { const {dispatch} = useSchema(); return ( -
- {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})} -
+ + {renderHeader({ + description: schema.description, + name, + path, + schema, + icon: , + onDelete: () => handleDelete(dispatch, path) + })} + + ); +}; + +SchemaPreview.Enum = function Enum({schema, path, data, name}: DataVisualizationType) { + const {dispatch} = useSchema(); + const enums = schema.enum + return ( + + {renderHeader({ + description: <>{schema.description} Options: {enums.map(e => ( + ))}, + name, + path, + schema, + icon: , + onDelete: () => handleDelete(dispatch, path) + })} + ); }; SchemaPreview.Number = function Number({schema, path, name}: DataVisualizationType) { const {dispatch} = useSchema(); return ( -
- {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})} -
+ + {renderHeader({ + description: schema.description, + name, + path, + schema, + icon: , + onDelete: () => handleDelete(dispatch, path) + })} + ); }; SchemaPreview.Boolean = function BooleanVisualization({schema, path, name}: DataVisualizationType) { const {dispatch} = useSchema(); return ( -
- {renderHeader({name, path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})} -
+ + {renderHeader({ + description: schema.description, + name, + path, + schema, + icon: , + onDelete: () => handleDelete(dispatch, path) + })} + ); }; @@ -189,9 +228,12 @@ SchemaPreview.Object = function ObjectVisualization({schema, path, data, name}: setOpen(!open); }; + const theme = useTheme(); + return ( - + {renderHeader({ + description: schema.description, name, path, schema, @@ -200,7 +242,7 @@ SchemaPreview.Object = function ObjectVisualization({schema, path, data, name}: onCollapse: handleCollapse, onDelete: () => handleDelete(dispatch, path) })} - + Properties @@ -210,36 +252,46 @@ SchemaPreview.Object = function ObjectVisualization({schema, path, data, name}: }} - {properties?.length > 0 ? properties?.map((property) => ( - <> - - - )) : ( - - Click on button to add properties - - )} + + {properties?.length > 0 ? properties?.map((property) => ( + <> + + + )) : ( + + Click on button to add properties + + )} + - - + + ); }; SchemaPreview.Array = function ArrayVisualization({schema, path, data, name}: DataVisualizationType) { const {dispatch} = useSchema(); + const theme = useTheme(); return ( - <> - - {renderHeader({name,path, schema, icon: , onDelete: () => handleDelete(dispatch, path)})} + + {renderHeader({ + description: schema.description, + name, + path, + schema, + icon: , + onDelete: () => handleDelete(dispatch, path) + })} + - - +
+ ); }; diff --git a/src/constants.ts b/src/constants.ts index c955ce9..1b44e0a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,7 @@ import {TimeField} from "./fields/widgets/TimeField"; import {DateTimeField} from "./fields/widgets/DateTimeField"; import {FaqWidget} from "./fields/patterns/FaqWidget"; import {FieldConfig} from "./types"; +import {SelectField} from "./fields/widgets/SelectField"; export const PRIMITIVE_PROPERTIES: FieldConfig[] = [ { @@ -72,6 +73,12 @@ export const STRING_WIDGETS: FieldConfig[] = [ title: 'Time Field', description: 'a time field', Class: TimeField, + }, + { + id: 'SELECT', + title: 'Select Field', + description: 'a select field with a list of options', + Class: SelectField, } ]; diff --git a/src/fields/JsonSchemaField.ts b/src/fields/JsonSchemaField.ts index a270016..7f42801 100644 --- a/src/fields/JsonSchemaField.ts +++ b/src/fields/JsonSchemaField.ts @@ -17,6 +17,11 @@ export class JsonSchemaField { protected writeOnly?: boolean; + protected enum?: string[]; + + protected enumNames?: string[]; + + constructor(name: string) { this.name = name; this.type = 'object'; @@ -46,6 +51,16 @@ export class JsonSchemaField { return this; } + setEnum(_enum: string[]): this { + this.enum = _enum; + return this; + } + + setEnumNames(enumNames: string[]): this { + this.enumNames = enumNames; + return this; + } + private setDefault(value: unknown): JsonSchemaField { this.default = value; return this; @@ -66,7 +81,12 @@ export class JsonSchemaField { return this; } - public setSchema(schema: SchemaAnnotation & { isRequired?: boolean }): void { + public setSchema(schema: SchemaAnnotation & { + isRequired?: boolean, options: { + enum: string; + enumNames: string; + }[] + }): void { if (schema.type) this.setType(schema.type) if (schema.title) this.setTitle(schema.title) if (schema.description) this.setDescription(schema.description) @@ -74,6 +94,14 @@ export class JsonSchemaField { if (schema.readOnly) this.setReadOnly(schema.readOnly) if (schema.writeOnly) this.setWriteOnly(schema.writeOnly) if (schema.isRequired) this.setIsRequired(schema.isRequired) + + const options = { + enum: schema.options?.map((option) => option.enum) || [], + enumNames: schema.options?.map((option) => option.enumNames) || [], + } + + if (options.enum?.length > 0) this.setEnum(options.enum) + if (options.enumNames?.length > 0) this.setEnumNames(options.enumNames) } @@ -84,7 +112,9 @@ export class JsonSchemaField { description: this.description, default: this.default, readOnly: this.readOnly, - writeOnly: this.writeOnly + writeOnly: this.writeOnly, + enum: this.enum, + enumNames: this.enumNames }; } @@ -121,6 +151,27 @@ export class JsonSchemaField { isRequired: { type: 'boolean', title: 'Field is required' + }, + options: { + type: "array", + title: "Options", + description: "Here you can add options for the select field", + items: { + type: "object", + properties: { + enum: { + type: this.getSchema()?.type || 'string', + title: "Value of the option", + description: "The value that is going to be in the form", + }, + enumNames: { + type: "string", + title: "Title of the option", + description: "The title that is going to be shown to user", + }, + }, + required: ["enum", "enumNames"], + } } }, required: ['title', 'type'], diff --git a/src/fields/primitives/NumberField.ts b/src/fields/primitives/NumberField.ts index 19faf97..afdeda8 100644 --- a/src/fields/primitives/NumberField.ts +++ b/src/fields/primitives/NumberField.ts @@ -97,5 +97,4 @@ export class NumberField extends JsonSchemaField { if (schema.exclusiveMaximum) this.setExclusiveMaximum(schema.exclusiveMaximum) if (schema.exclusiveMinimum) this.setExclusiveMinimum(schema.exclusiveMinimum) } - } diff --git a/src/fields/widgets/SelectField.ts b/src/fields/widgets/SelectField.ts new file mode 100644 index 0000000..3bd78a7 --- /dev/null +++ b/src/fields/widgets/SelectField.ts @@ -0,0 +1,83 @@ +import {StringField, StringFieldType} from "../primitives/StringField"; +import {JsonSchema} from "../../types"; +import {produce} from "immer"; + +export type SelectFieldType = StringFieldType & { + options: { + enum: string; + enumNames: string; + }[] +}; + +export class SelectField extends StringField { + protected enum?: string[]; + + protected enumNames?: string[]; + + constructor(name: string) { + super(name); + } + + setEnum(_enum: string[]): this { + this.enum = _enum; + return this; + } + + setEnumNames(enumNames: string[]): this { + this.enumNames = enumNames; + return this; + } + + getBuilderSchema(): JsonSchema { + const enumSchema: Record = { + options: { + type: "array", + title: "Options", + minItems: 1, + description: "Here you can add options for the select field", + items: { + type: "object", + properties: { + enum: { + type: "string", + title: "Value of the option", + description: "The value that is going to be in the form", + }, + enumNames: { + type: "string", + title: "Title of the option", + description: "The title that is going to be shown to user", + }, + }, + required: ["enum", "enumNames"], + } + } + } + + return produce(super.getBuilderSchema(), (draft: JsonSchema) => { + Object.keys(enumSchema).forEach(key => { + if (draft.properties) draft.properties[key] = enumSchema[key]; + }) + }); + } + + public getSchema(): JsonSchema { + return { + ...super.getSchema(), + enum: this.enum, + enumNames: this.enumNames, + } + } + + public setSchema(schema: SelectFieldType): void { + super.setSchema(schema); + + const options = { + enum: schema.options?.map((option) => option.enum) || [], + enumNames: schema.options?.map((option) => option.enumNames) || [], + } + + if (options.enum?.length > 0) this.setEnum(options.enum) + if (options.enumNames?.length > 0) this.setEnumNames(options.enumNames) + } +} diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index 6f0b8f6..6cfc26b 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -23,12 +23,34 @@ const sampleSchema: RJSFSchema = { "type": "string", "minLength": 1 }, + "type": { + "title": "Type", + "description": "The type of the item.", + "type": "string", + enum: ['grocery', 'cloths'], + enumNames: ['Grocery', 'Cloths'], + }, "price": { "title": "Price", "description": "The price of the item.", "type": "number", "minimum": 0 }, + "location": { + "title": "Location", + "description": "The coordination.", + "type": "object", + properties: { + lat: { + type: 'number', + title: "latitude" + }, + long: { + type: 'number', + title: "longitude" + }, + } + }, "tags": { "title": "Tags", "description": "Tags associated with the item.", @@ -124,6 +146,13 @@ Themed.args = { main: '#ff5722' }, mode: 'dark' + }, + components: { + MuiInput: { + defaultProps: { + size: 'small' + } + } } } }; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 2fa9f34..a3bba91 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,7 +48,7 @@ export interface SchemaAnnotation { default?: unknown; readOnly?: boolean; writeOnly?: boolean; - enum?: unknown[]; + enum?: string[]; // TODO: Generalize enum values enumNames?: string[]; } diff --git a/src/utils.ts b/src/utils.ts index 92f62a5..f3d8ea4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -22,6 +22,7 @@ export const getSchemaFormatFromSchema = ( Unknown({schema, data}: DataVisualizationType): JSX.Element; }, ) => { + if (schema?.enum?.length > 0) return SchemaFormat.Enum; 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; @@ -30,7 +31,6 @@ export const getSchemaFormatFromSchema = ( // 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;