From 61105e98f8ab5fc368021bb1a74fbd3739b557a3 Mon Sep 17 00:00:00 2001 From: mohammadreza pakzadian Date: Tue, 23 Jul 2024 12:36:26 +0330 Subject: [PATCH 1/5] feat: add template type --- .husky/pre-commit | 3 +- src/components/AddFieldModal.tsx | 43 ++++++++---- src/constants.ts | 17 +---- src/fields/patterns/FaqWidget.ts | 42 ------------ src/providers/SchemaProvider.tsx | 9 ++- src/stories/SchemaBuilder.stories.tsx | 98 ++++++++++++++++++++++++++- src/types.ts | 7 ++ 7 files changed, 141 insertions(+), 78 deletions(-) delete mode 100644 src/fields/patterns/FaqWidget.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index bb76f84..b14d371 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1 @@ -pnpm lint-staged -pnpm test +pnpm lint-staged && pnpm test diff --git a/src/components/AddFieldModal.tsx b/src/components/AddFieldModal.tsx index 550a764..110f5e7 100644 --- a/src/components/AddFieldModal.tsx +++ b/src/components/AddFieldModal.tsx @@ -4,7 +4,7 @@ import Form from '@rjsf/mui'; import validator from '@rjsf/validator-ajv8'; import React from 'react'; import { useSchema } from '../providers/SchemaProvider'; -import { JsonSchema, JsonSchemaType } from '../types'; +import { JsonSchema } from '../types'; import { JsonSchemaField } from '../fields/JsonSchemaField'; import Select from '@mui/material/Select'; import { Add } from '@mui/icons-material'; @@ -17,18 +17,38 @@ type Props = { const AddFieldModal = ({ parentPath }: Props) => { const [open, setOpen] = React.useState(false); const [name, setName] = React.useState(null); - const [type, setType] = React.useState(null); + const [type, setType] = React.useState(null); const [field, setField] = React.useState(null); - const { dispatch, fields } = useSchema(); + const { dispatch, fields, templates } = useSchema(); const [step, setStep] = React.useState(0); - const SelectedFieldClass = fields.find((f) => f.id === type)?.Class; - const handleSelectType = () => { + const SelectedFieldClass = fields.find((f) => f.id === type)?.Class; if (name && type && SelectedFieldClass) { setField(new SelectedFieldClass(name)); setStep(1); } + + const SelectedTemplateSchema = templates.find((f) => f.id === type)?.schema; + if (name && type && SelectedTemplateSchema) { + dispatch({ + type: 'ADD_PROPERTY', + payload: { + name: generatePath(parentPath, name || 'newField'), + schema: SelectedTemplateSchema, + }, + }); + dispatch({ + type: 'ADD_REQUIRED', + payload: { + name: generatePath(parentPath, name || 'newField'), + }, + }); + setOpen(false); + setType(null); + setName(null); + setStep(0); + } }; const handleSubmit = (formData: JsonSchema) => { @@ -85,18 +105,17 @@ const AddFieldModal = ({ parentPath }: Props) => { Field Type - setType(e.target.value)}> {fields.map((property) => ( {property.title} ))} + {templates?.map((property) => ( + + {property.title} + + ))} diff --git a/src/constants.ts b/src/constants.ts index 727e90b..019df52 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,7 +7,6 @@ import { ArrayField } from './fields/containers/ArrayField'; import { DateField } from './fields/widgets/DateField'; 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'; @@ -89,18 +88,4 @@ export const STRING_WIDGETS: FieldConfig[] = [ }, ]; -export const PATTERNS: FieldConfig[] = [ - { - id: 'FAQ', - title: 'FAQ', - description: 'a FAQ form', - Class: FaqWidget, - }, -]; - -export const PROPERTIES = [ - ...PRIMITIVE_PROPERTIES, - ...CONTAINER_PROPERTIES, - // ...STRING_WIDGETS, - // ...PATTERNS, -]; +export const PROPERTIES = [...PRIMITIVE_PROPERTIES, ...CONTAINER_PROPERTIES, ...STRING_WIDGETS]; diff --git a/src/fields/patterns/FaqWidget.ts b/src/fields/patterns/FaqWidget.ts deleted file mode 100644 index f53c1fa..0000000 --- a/src/fields/patterns/FaqWidget.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { StringField } from '../primitives/StringField'; -import { ObjectField, ObjectFieldType } from '../containers/ObjectField'; -import { produce } from 'immer'; -import { ArrayField } from '../containers/ArrayField'; -import { JsonSchema } from '../../types'; -import { SCHEMA_TYPE } from '../../constants'; - -export type FaqType = ObjectFieldType; - -export class FaqWidget extends ArrayField { - constructor(name: string) { - super(name); - this.title = 'Frequently Asked Questions'; - this.setSchema({ - items: { - // @ts-expect-error TODO: fix - question: { - type: SCHEMA_TYPE.STRING, - title: 'FAQ Question', - }, - answer: { - type: SCHEMA_TYPE.STRING, - title: 'FAQ Answer', - }, - }, - }); - } - - getBuilderSchema(): JsonSchema { - // TODO: fix - return produce(super.getBuilderSchema(), (draft: JsonSchema) => { - const items = new ObjectField('items'); - items.setSchema({ - properties: { - question: new StringField('question').getBuilderSchema(), - answers: new StringField('answers').getBuilderSchema(), - }, - }); - if (draft?.items) draft.items = items.getBuilderSchema(); - }); - } -} diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index ed8041f..6b63f3a 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -1,6 +1,6 @@ import React, { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; import { JsonSchemaBuilder } from '../builder/JsonSchemaBuilder'; -import { FieldConfig, JsonSchema } from '../types'; +import { FieldConfig, JsonSchema, TemplateType } from '../types'; import { PROPERTIES } from '../constants'; import type { RJSFSchema } from '@rjsf/utils'; @@ -13,10 +13,12 @@ export const SchemaContext = createContext<{ schema: JsonSchema; dispatch: Dispatch; fields: FieldConfig[]; + templates: TemplateType[]; }>({ schema: new JsonSchemaBuilder().setType('object').build(), dispatch: () => null, fields: [], + templates: [], }); const schemaReducer = (state: JsonSchema, action: SchemaAction): JsonSchema => { @@ -49,13 +51,14 @@ type Props = { extraFields: FieldConfig[]; children: ReactNode; value?: RJSFSchema; + templates?: TemplateType[]; }; -export const SchemaProvider = ({ children, extraFields, value }: Props) => { +export const SchemaProvider = ({ children, extraFields, value, templates }: Props) => { const [schema, dispatch] = useReducer(schemaReducer, value || new JsonSchemaBuilder().setType('object').build()); return ( - + {children} ); diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index e33e8df..75c4afe 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import React from 'react'; -import { Story, Meta } from '@storybook/react'; +import { Story } from '@storybook/react'; import SchemaBuilder from '../components/SchemaBuilder'; import { STRING_WIDGETS } from '../constants'; import { SchemaProvider } from '../providers/SchemaProvider'; @@ -98,13 +98,105 @@ const sampleSchema: RJSFSchema = { additionalProperties: false, }; +const template = { + id: 'Example_Schema', + title: 'Example Schema', + description: 'A rich JSON schema example without dependencies and no nested objects.', + schema: { + 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, + }, + 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.', + type: 'array', + items: { + 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, + }, + birthday: { + title: 'Birthday Date', + type: 'string', + minimum: 0, + format: 'date', + }, + inStock: { + title: 'In Stock', + description: 'Indicates if the item is in stock.', + type: 'boolean', + }, + }, + required: ['id', 'name', 'price'], + }, +}; + export default { title: 'SchemaBuilder', component: SchemaBuilder, -} as Meta; +}; const Template: Story = (args) => ( - + ); diff --git a/src/types.ts b/src/types.ts index 27d0e92..507bdd6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,6 +60,13 @@ export type FieldConfig = { Class: new (...args: string[]) => JsonSchemaField; // a class that extends JsonSchemaField }; +export type TemplateType = { + id: string; + title: string; + description: string; + schema: JsonSchema; +}; + export type DataVisualizationType = { schema: RJSFSchema; data: T; From 4f90bd6197becfbe33cd353a6093095f0dd4b5b2 Mon Sep 17 00:00:00 2001 From: mohammadreza pakzadian Date: Tue, 23 Jul 2024 14:13:48 +0330 Subject: [PATCH 2/5] feat: add template story to storybook --- src/stories/SchemaBuilder.stories.tsx | 131 +++++++------------------- 1 file changed, 36 insertions(+), 95 deletions(-) diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index 75c4afe..2c30b19 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -98,113 +98,21 @@ const sampleSchema: RJSFSchema = { additionalProperties: false, }; -const template = { - id: 'Example_Schema', - title: 'Example Schema', - description: 'A rich JSON schema example without dependencies and no nested objects.', - schema: { - 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, - }, - 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.', - type: 'array', - items: { - 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, - }, - birthday: { - title: 'Birthday Date', - type: 'string', - minimum: 0, - format: 'date', - }, - inStock: { - title: 'In Stock', - description: 'Indicates if the item is in stock.', - type: 'boolean', - }, - }, - required: ['id', 'name', 'price'], - }, -}; - export default { title: 'SchemaBuilder', component: SchemaBuilder, }; -const Template: Story = (args) => ( +const PrimitivesTemplate: Story = (args) => ( ); -export const Primitives = Template.bind({}); +export const Primitives = PrimitivesTemplate.bind({}); Primitives.args = {}; -export const Formats = Template.bind({}); +export const Formats = PrimitivesTemplate.bind({}); Formats.args = { extraFields: [...STRING_WIDGETS], }; @@ -250,3 +158,36 @@ Themed.args = { }, }, }; + +const customTemplate = { + id: 'FAQ_TEMPLATE', + title: 'FAQ Template', + description: 'A template schema for FAQ type.', + schema: { + title: 'FAQ', + type: 'array', + items: { + type: 'object', + title: 'List of Questions', + properties: { + question: { + title: 'question', + type: 'string', + }, + answer: { + title: 'answer', + type: 'string', + }, + }, + }, + uniqueItems: true, + }, +}; + +const FaqTemplate: Story = (args) => ( + + + +); + +export const CustomTemplate = FaqTemplate.bind({}); From 7e437c412c7fbe949642e8ce20a5f9688de36c53 Mon Sep 17 00:00:00 2001 From: mohammadreza pakzadian Date: Tue, 23 Jul 2024 14:25:57 +0330 Subject: [PATCH 3/5] test: add unit test for template type --- src/test/JsonSchemaBuilder.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/test/JsonSchemaBuilder.test.ts b/src/test/JsonSchemaBuilder.test.ts index 3e599ee..afb2314 100644 --- a/src/test/JsonSchemaBuilder.test.ts +++ b/src/test/JsonSchemaBuilder.test.ts @@ -165,4 +165,29 @@ describe('JsonSchemaBuilder', () => { const schema = builderWithInput.build(); expect(schema).toEqual(inputSchema); }); + + test('should add template schema', () => { + const templateSchema = { + title: 'FAQ', + type: 'array', + items: { + type: 'object', + title: 'List of Questions', + properties: { + question: { + title: 'question', + type: 'string', + }, + answer: { + title: 'answer', + type: 'string', + }, + }, + }, + uniqueItems: true, + }; + builder.addProperty('faq', templateSchema); + const schema = builder.build(); + expect(schema.properties?.faq).toEqual(templateSchema); + }); }); From 85c8aab8ebbf12ae3284b972b4cf855150b38869 Mon Sep 17 00:00:00 2001 From: mohammadreza pakzadian Date: Tue, 23 Jul 2024 14:28:50 +0330 Subject: [PATCH 4/5] fix: add default value to templates --- src/providers/SchemaProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index 6b63f3a..6943027 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -54,11 +54,11 @@ type Props = { templates?: TemplateType[]; }; -export const SchemaProvider = ({ children, extraFields, value, templates }: Props) => { +export const SchemaProvider = ({ children, extraFields, value, templates = [] }: Props) => { const [schema, dispatch] = useReducer(schemaReducer, value || new JsonSchemaBuilder().setType('object').build()); return ( - + {children} ); From c273081d7064f0a94c9f94d53cca759a6b93e59e Mon Sep 17 00:00:00 2001 From: mohammadreza pakzadian Date: Tue, 23 Jul 2024 14:49:38 +0330 Subject: [PATCH 5/5] fix: make extraFields property optional with default value --- src/providers/SchemaProvider.tsx | 4 ++-- src/stories/SchemaBuilder.stories.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/providers/SchemaProvider.tsx b/src/providers/SchemaProvider.tsx index 6943027..0119b26 100644 --- a/src/providers/SchemaProvider.tsx +++ b/src/providers/SchemaProvider.tsx @@ -48,13 +48,13 @@ const schemaReducer = (state: JsonSchema, action: SchemaAction): JsonSchema => { }; type Props = { - extraFields: FieldConfig[]; children: ReactNode; value?: RJSFSchema; templates?: TemplateType[]; + extraFields?: FieldConfig[]; }; -export const SchemaProvider = ({ children, extraFields, value, templates = [] }: Props) => { +export const SchemaProvider = ({ children, extraFields = [], value, templates = [] }: Props) => { const [schema, dispatch] = useReducer(schemaReducer, value || new JsonSchemaBuilder().setType('object').build()); return ( diff --git a/src/stories/SchemaBuilder.stories.tsx b/src/stories/SchemaBuilder.stories.tsx index 2c30b19..17f61da 100644 --- a/src/stories/SchemaBuilder.stories.tsx +++ b/src/stories/SchemaBuilder.stories.tsx @@ -185,7 +185,7 @@ const customTemplate = { }; const FaqTemplate: Story = (args) => ( - + );