diff --git a/packages/client/src/api-client/APIClient.ts b/packages/client/src/api-client/APIClient.ts index 4a6145472f..8fb91424b2 100644 --- a/packages/client/src/api-client/APIClient.ts +++ b/packages/client/src/api-client/APIClient.ts @@ -19,6 +19,7 @@ export interface IResource { create?: (params?: ActionParams) => Promise; update?: (params?: ActionParams) => Promise; destroy?: (params?: ActionParams) => Promise; + [key: string]: (params?: ActionParams) => Promise; } export class APIClient { diff --git a/packages/client/src/schema-component/antd/grid/AddBlockItem.tsx b/packages/client/src/schema-component/antd/grid/AddBlockItem.tsx index 04a645e471..e136abf715 100644 --- a/packages/client/src/schema-component/antd/grid/AddBlockItem.tsx +++ b/packages/client/src/schema-component/antd/grid/AddBlockItem.tsx @@ -103,6 +103,96 @@ const FormBlockInitializerItem = itemWrap((props) => { ); }); +const TestInitializerItem = itemWrap((props) => { + const { insert } = props; + return ( + } + onClick={() => { + insert({ + type: 'void', + name: uid(), + 'x-decorator': 'CardItem', + 'x-component': 'Grid', + 'x-uid': uid(), + properties: { + row1: { + type: 'void', + 'x-component': 'Grid.Row', + 'x-uid': uid(), + properties: { + col11: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block1: { + type: 'void', + title: '1', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + }, + block2: { + type: 'void', + title: '2', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + }, + }, + }, + col12: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block3: { + type: 'void', + title: '3', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + }, + }, + }, + }, + }, + row2: { + type: 'void', + 'x-component': 'Grid.Row', + 'x-uid': uid(), + properties: { + col21: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block4: { + type: 'void', + title: '4', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + }, + }, + }, + col22: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block5: { + type: 'void', + title: '5', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + }, + }, + }, + }, + }, + }, + }); + }} + > + Test + + ); +}); + export const AddGridBlockItem = observer((props: any) => { return ( { title: 'Form', component: FormBlockInitializerItem, }, + { + type: 'item', + title: 'Test', + component: TestInitializerItem, + }, ], }, ]} diff --git a/packages/client/src/schema-component/antd/grid/AddFormItem.tsx b/packages/client/src/schema-component/antd/grid/AddFormItem.tsx index 92c9e54b3d..50a89f00d5 100644 --- a/packages/client/src/schema-component/antd/grid/AddFormItem.tsx +++ b/packages/client/src/schema-component/antd/grid/AddFormItem.tsx @@ -74,8 +74,10 @@ const useCurrentFieldSchema = (path: string) => { remove() { schema && remove(schema, { - breakComponent: 'Grid', - removeEmptyParents: true, + removeParentsIfNoChildren: true, + breakSchema: (s) => { + return s['x-component'] === 'Grid'; + }, }); }, }; diff --git a/packages/client/src/schema-component/antd/grid/Block.tsx b/packages/client/src/schema-component/antd/grid/Block.tsx new file mode 100644 index 0000000000..210bc4905f --- /dev/null +++ b/packages/client/src/schema-component/antd/grid/Block.tsx @@ -0,0 +1,13 @@ +import { observer, useFieldSchema } from '@formily/react'; +import React from 'react'; +import { DragHandler } from '../../'; + +export const Block = observer((props) => { + const fieldSchema = useFieldSchema(); + return ( +
+ Block {fieldSchema.title} + +
+ ); +}); diff --git a/packages/client/src/schema-component/antd/grid/demos/demo1.tsx b/packages/client/src/schema-component/antd/grid/demos/demo1.tsx index 774310d55d..1fc91654bc 100644 --- a/packages/client/src/schema-component/antd/grid/demos/demo1.tsx +++ b/packages/client/src/schema-component/antd/grid/demos/demo1.tsx @@ -1,5 +1,4 @@ import { observer, useFieldSchema } from '@formily/react'; -import { uid } from '@formily/shared'; import { BlockItem, DragHandler, Grid, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import React from 'react'; @@ -13,87 +12,151 @@ const Block = observer((props) => { ); }); +const schema = { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + name: 'grid1', + 'x-decorator': 'CardItem', + 'x-component': 'Grid', + properties: { + row1: { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + col11: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '1', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'a9m97uffyku', + 'x-async': false, + 'x-index': 1, + }, + block2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '2.1', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'lensw462z8w', + 'x-async': false, + 'x-index': 2, + }, + block234: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '2.2', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'lensw462z81', + 'x-async': false, + 'x-index': 3, + }, + }, + 'x-uid': '4shevom50rl', + 'x-async': false, + 'x-index': 1, + }, + col12: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '3', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'kp4kjknbs2l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u7n3beze0gr', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'qfl04eq71tt', + 'x-async': false, + 'x-index': 1, + }, + row2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + col21: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block4: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '4', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'v5bd5kcyhat', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'bwg0mv89atk', + 'x-async': false, + 'x-index': 1, + }, + col22: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + block5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '5', + 'x-decorator': 'BlockItem', + 'x-component': 'Block', + 'x-uid': 'noh1flz5oqg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oz0rypr2rlj', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '5igqpmvilz1', + 'x-async': false, + 'x-index': 2, + }, + }, +}; + export default function App() { return ( - + ); } diff --git a/packages/client/src/schema-component/antd/grid/demos/demo3.tsx b/packages/client/src/schema-component/antd/grid/demos/demo3.tsx index c080fbe1ad..4bbe29673d 100644 --- a/packages/client/src/schema-component/antd/grid/demos/demo3.tsx +++ b/packages/client/src/schema-component/antd/grid/demos/demo3.tsx @@ -1,20 +1,44 @@ -import { ISchema } from '@formily/react'; +import { ISchema, observer, useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared'; -import { CardItem, Form, Grid, Markdown, SchemaComponent, SchemaComponentProvider, VoidTable } from '@nocobase/client'; +import { + BlockItem, + CardItem, + DragHandler, + Form, + Grid, + Markdown, + SchemaComponent, + SchemaComponentProvider, + VoidTable +} from '@nocobase/client'; import React from 'react'; +const Block = observer((props) => { + const fieldSchema = useFieldSchema(); + return ( +
+ Block {fieldSchema.title} + +
+ ); +}); + const schema: ISchema = { - type: 'void', - name: 'grid1', - 'x-component': 'Grid', - 'x-item-initializer': 'Grid.AddBlockItem', - 'x-uid': uid(), - properties: {}, + type: 'object', + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-item-initializer': 'Grid.AddBlockItem', + 'x-uid': uid(), + properties: {}, + }, + }, }; export default function App() { return ( - + ); diff --git a/packages/client/src/schema-component/antd/grid/index.tsx b/packages/client/src/schema-component/antd/grid/index.tsx index 762542e7cb..17fa0f85f3 100644 --- a/packages/client/src/schema-component/antd/grid/index.tsx +++ b/packages/client/src/schema-component/antd/grid/index.tsx @@ -1 +1,3 @@ +export * from './Block'; export * from './Grid'; + diff --git a/packages/client/src/schema-component/common/dnd-context/index.tsx b/packages/client/src/schema-component/common/dnd-context/index.tsx index b5c45a92e9..0bc934ced0 100644 --- a/packages/client/src/schema-component/common/dnd-context/index.tsx +++ b/packages/client/src/schema-component/common/dnd-context/index.tsx @@ -7,7 +7,6 @@ const useDragEnd = () => { const { refresh } = useDesignable(); return ({ active, over }: DragEndEvent) => { - console.log({ active, over }); const activeSchema = active?.data?.current?.schema; const overSchema = over?.data?.current?.schema; const insertAdjacent = over?.data?.current?.insertAdjacent; @@ -35,7 +34,7 @@ const useDragEnd = () => { if (insertAdjacent) { dn.insertAdjacent(insertAdjacent, activeSchema, { wrap: wrapSchema, - removeEmptyParents: true, + removeParentsIfNoChildren: true, }); return; } diff --git a/packages/client/src/schema-component/common/sortable-item/SortableItem.tsx b/packages/client/src/schema-component/common/sortable-item/SortableItem.tsx index 0f9517ef8e..18b07abfc0 100644 --- a/packages/client/src/schema-component/common/sortable-item/SortableItem.tsx +++ b/packages/client/src/schema-component/common/sortable-item/SortableItem.tsx @@ -34,16 +34,11 @@ export const Sortable = (props: any) => { export const SortableItem: React.FC = observer((props) => { const field = useField(); const fieldSchema = useFieldSchema(); - const onInsertAdjacent = ({ dn, orginDraggedParentSchema }) => { - dn.removeIfChildrenEmpty(orginDraggedParentSchema, { - removeEmptyParents: true, - }); - }; return ( {props.children} diff --git a/packages/client/src/schema-component/hooks/__tests__/designable.test.ts b/packages/client/src/schema-component/hooks/__tests__/designable.test.ts new file mode 100644 index 0000000000..ccb5b7d728 --- /dev/null +++ b/packages/client/src/schema-component/hooks/__tests__/designable.test.ts @@ -0,0 +1,453 @@ +import { Schema } from '@formily/react'; +import { createDesignable, Designable } from '../useDesignable'; + +describe('createDesignable', () => { + let dn: Designable; + let schema: Schema; + + beforeEach(() => { + schema = new Schema({ + type: 'void', + name: 'global', + 'x-uid': 'global', + properties: { + current: { + type: 'void', + 'x-uid': 'current', + properties: { + child: { + type: 'void', + 'x-uid': 'child', + }, + }, + }, + }, + }); + dn = createDesignable({ + current: schema.properties.current, + }); + }); + + describe('insert a new node', () => { + test('insertBeforeBegin', () => { + dn.insertBeforeBegin({ + name: 'abc', + }); + expect(schema.toJSON()).toMatchObject({ + type: 'void', + name: 'global', + 'x-uid': 'global', + properties: { + abc: { + name: 'abc', + type: 'void', + }, + current: { + type: 'void', + name: 'current', + 'x-uid': 'current', + properties: { + child: { + type: 'void', + 'x-uid': 'child', + }, + }, + }, + }, + }); + expect(schema.properties.abc['x-uid']).toBeDefined(); + expect(Object.keys(schema.properties)).toEqual(['abc', 'current']); + }); + + test('insertAfterBegin', () => { + dn.insertAfterBegin({ + name: 'abc', + }); + expect(Object.keys(schema.properties.current.properties)).toEqual(['abc', 'child']); + expect(schema.toJSON()).toMatchObject({ + type: 'void', + name: 'global', + 'x-uid': 'global', + properties: { + current: { + type: 'void', + name: 'current', + 'x-uid': 'current', + properties: { + abc: { + name: 'abc', + type: 'void', + }, + child: { + type: 'void', + 'x-uid': 'child', + }, + }, + }, + }, + }); + expect(schema.properties.current.properties.abc['x-uid']).toBeDefined(); + }); + + test('insertBeforeEnd', () => { + dn.insertBeforeEnd({ + name: 'abc', + }); + expect(schema.toJSON()).toMatchObject({ + type: 'void', + name: 'global', + 'x-uid': 'global', + properties: { + current: { + type: 'void', + name: 'current', + 'x-uid': 'current', + properties: { + child: { + type: 'void', + 'x-uid': 'child', + }, + abc: { + name: 'abc', + type: 'void', + }, + }, + }, + }, + }); + expect(schema.properties.current.properties.abc['x-uid']).toBeDefined(); + }); + + test('insertAfterEnd', () => { + dn.insertAfterEnd({ + name: 'abc', + }); + expect(schema.toJSON()).toMatchObject({ + type: 'void', + name: 'global', + 'x-uid': 'global', + properties: { + current: { + type: 'void', + name: 'current', + 'x-uid': 'current', + properties: { + child: { + type: 'void', + 'x-uid': 'child', + }, + }, + }, + abc: { + name: 'abc', + type: 'void', + }, + }, + }); + expect(schema.properties.abc['x-uid']).toBeDefined(); + expect(Object.keys(schema.properties)).toEqual(['current', 'abc']); + }); + }); + + describe('move an existing node', () => { + test('insertBeforeBegin', () => { + dn.insertBeforeBegin(schema.properties.current.properties.child); + expect(Object.keys(schema.properties)).toEqual(['child', 'current']); + expect(schema.properties.current?.properties?.child).toBeUndefined(); + }); + + test('insertBeforeBegin', () => { + dn.insertBeforeBegin({ + type: 'void', + name: 'a1', + }); + dn.insertBeforeBegin({ + type: 'void', + name: 'a2', + }); + dn.insertBeforeBegin({ + type: 'void', + name: 'a3', + }); + expect(Object.keys(schema.properties)).toEqual(['a1', 'a2', 'a3', 'current']); + dn.insertBeforeBegin(schema.properties.a2); + expect(Object.keys(schema.properties)).toEqual(['a1', 'a3', 'a2', 'current']); + }); + + test('insertAfterBegin', () => { + dn.insertBeforeBegin({ + name: 'a1', + }); + dn.insertAfterBegin(schema.properties.a1); + expect(Object.keys(schema.properties.current.properties)).toEqual(['a1', 'child']); + expect(schema.properties.a1).toBeUndefined(); + }); + + test('insertBeforeEnd', () => { + dn.insertBeforeBegin({ + name: 'a1', + }); + dn.insertBeforeEnd(schema.properties.a1); + expect(Object.keys(schema.properties.current.properties)).toEqual(['child', 'a1']); + expect(schema.properties.a1).toBeUndefined(); + }); + + test('insertAfterEnd', () => { + dn.insertAfterEnd(schema.properties.current.properties.child); + expect(Object.keys(schema.properties)).toEqual(['current', 'child']); + expect(schema.properties.current?.properties?.child).toBeUndefined(); + }); + + test('insertAfterEnd', () => { + dn.insertAfterEnd({ + type: 'void', + name: 'a1', + }); + dn.insertAfterEnd({ + type: 'void', + name: 'a2', + }); + dn.insertAfterEnd({ + type: 'void', + name: 'a3', + }); + expect(Object.keys(schema.properties)).toEqual(['current', 'a3', 'a2', 'a1']); + dn.insertAfterEnd(schema.properties.a2); + expect(Object.keys(schema.properties)).toEqual(['current', 'a2', 'a3', 'a1']); + }); + }); + + describe('removeIfNoChildren', () => { + test('has child nodes', () => { + dn.removeIfNoChildren(schema.properties.current); + expect(schema.properties.current).toBeDefined(); + }); + + test('no child nodes', () => { + dn.removeIfNoChildren(schema.properties.current.properties.child); + expect(schema.properties.current?.properties?.child).toBeUndefined(); + }); + }); + + describe('remove', () => { + test('without schema', () => { + dn.remove(); + expect(schema?.properties?.current).toBeUndefined(); + }); + + test('without options', () => { + dn.remove(schema.properties.current.properties.child); + expect(schema.properties.current).toBeDefined(); + expect(schema.properties.current?.properties?.child).toBeUndefined(); + }); + + test('removeParentsIfNoChildren + breakSchema json', () => { + dn.remove(schema.properties.current.properties.child, { + removeParentsIfNoChildren: true, + breakSchema: { + 'x-uid': 'global', + }, + }); + expect(schema?.properties?.current).toBeUndefined(); + }); + + test('removeParentsIfNoChildren + breakSchema function', () => { + dn.remove(schema.properties.current.properties.child, { + removeParentsIfNoChildren: true, + breakSchema: (s) => s['x-uid'] === 'global', + }); + expect(schema?.properties?.current).toBeUndefined(); + }); + }); +}); + +describe('x-index', () => { + let dn: Designable; + let schema: Schema; + + beforeEach(() => { + schema = new Schema({ + type: 'void', + name: 'col1', + 'x-uid': 'col1', + properties: { + block1: { + type: 'void', + 'x-index': 1, + }, + block2: { + type: 'void', + 'x-index': 2, + }, + block3: { + type: 'void', + 'x-index': 3, + }, + block4: { + type: 'void', + 'x-index': 4, + }, + block5: { + type: 'void', + 'x-index': 5, + }, + }, + }); + }); + + test('insertBeforeBeginOrAfterEnd', () => { + dn = createDesignable({ + current: schema.properties.block2, + }); + dn.insertBeforeBeginOrAfterEnd(schema.properties.block5); + expect(Object.keys(schema.properties)).toEqual(['block1', 'block5', 'block2', 'block3', 'block4']); + }); + + test('insertBeforeBegin', () => { + dn = createDesignable({ + current: schema.properties.block2, + }); + dn.insertBeforeBegin({ + name: 'block0', + }); + expect(Object.keys(schema.properties)).toEqual(['block1', 'block0', 'block2', 'block3', 'block4', 'block5']); + }); + + test('insertAfterEnd', () => { + dn = createDesignable({ + current: schema.properties.block2, + }); + dn.insertAfterEnd({ + name: 'block0', + }); + expect(Object.keys(schema.properties)).toEqual(['block1', 'block2', 'block0', 'block3', 'block4', 'block5']); + }); + + test('insertAfterBegin', () => { + dn = createDesignable({ + current: schema, + }); + dn.insertAfterBegin({ + name: 'block0', + }); + expect(Object.keys(schema.properties)).toEqual(['block0', 'block1', 'block2', 'block3', 'block4', 'block5']); + }); + + test('insertBeforeEnd', () => { + dn = createDesignable({ + current: schema, + }); + dn.insertBeforeEnd({ + name: 'block0', + }); + expect(Object.keys(schema.properties)).toEqual(['block1', 'block2', 'block3', 'block4', 'block5', 'block0']); + }); +}); + +describe('wrap', () => { + let dn: Designable; + let schema: Schema; + const colWrap = (s) => { + return { + name: 'col12', + type: 'void', + 'x-uid': 'col12', + properties: { + [s.name]: s, + }, + }; + }; + + const rowWrap = (s) => { + return { + name: 'row2', + type: 'void', + 'x-uid': 'row2', + properties: { + col21: { + type: 'void', + 'x-uid': 'col21', + properties: { + [s.name]: s, + }, + }, + }, + }; + }; + + beforeEach(() => { + schema = new Schema({ + type: 'void', + name: 'grid1', + 'x-uid': 'grid1', + properties: { + row1: { + type: 'void', + 'x-uid': 'row1', + properties: { + col11: { + type: 'void', + 'x-uid': 'col11', + properties: { + block1: { + type: 'void', + }, + }, + }, + }, + }, + }, + }); + }); + + test('insertBeforeBegin', () => { + dn = createDesignable({ + current: schema.properties.row1.properties.col11, + }); + const s = { + type: 'void', + name: 'block2', + }; + dn.insertBeforeBegin(s, { + wrap: colWrap, + }); + expect(Object.keys(schema.properties.row1.properties)).toEqual(['col12', 'col11']); + expect(schema.properties.row1.properties.col12.properties.block2).toBeDefined(); + }); + + test('insertAfterEnd', () => { + dn = createDesignable({ + current: schema.properties.row1.properties.col11, + }); + const s = { + type: 'void', + name: 'block2', + }; + dn.insertAfterEnd(s, { + wrap: colWrap, + }); + expect(Object.keys(schema.properties.row1.properties)).toEqual(['col11', 'col12']); + expect(schema.properties.row1.properties.col12.properties.block2).toBeDefined(); + }); + + test('removeParentsIfNoChildren', () => { + const coldn = createDesignable({ + current: schema.properties.row1.properties.col11, + }); + const s = { + type: 'void', + name: 'block2', + }; + coldn.insertAfterEnd(s, { wrap: colWrap }); + const griddn = createDesignable({ + current: schema, + }); + griddn.insertBeforeEnd(schema.properties.row1.properties.col12.properties.block2, { + wrap: rowWrap, + removeParentsIfNoChildren: true, + }); + expect(Object.keys(schema.properties.row1.properties)).toEqual(['col11']); + expect(Object.keys(schema.properties.row2.properties)).toEqual(['col21']); + expect(schema.properties.row2.properties.col21.properties.block2).toBeDefined(); + }); +}); diff --git a/packages/client/src/schema-component/hooks/useDesignable.tsx b/packages/client/src/schema-component/hooks/useDesignable.tsx index a26ffcb9df..c9bf2165fd 100644 --- a/packages/client/src/schema-component/hooks/useDesignable.tsx +++ b/packages/client/src/schema-component/hooks/useDesignable.tsx @@ -3,6 +3,7 @@ import { uid } from '@formily/shared'; import get from 'lodash/get'; import set from 'lodash/set'; import React, { useContext } from 'react'; +import { useAPIClient } from '../../api-client'; import { SchemaComponentContext } from '../context'; interface CreateDesignableProps { @@ -20,12 +21,14 @@ type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; interface InsertAdjacentOptions { wrap?: (s: ISchema) => ISchema; - removeEmptyParents?: boolean; + removeParentsIfNoChildren?: boolean; } +type BreakFn = (s: ISchema) => boolean; + interface RemoveOptions { - removeEmptyParents?: boolean; - breakComponent?: string; + removeParentsIfNoChildren?: boolean; + breakSchema?: ISchema | BreakFn; } const generateUid = (s: ISchema) => { @@ -76,7 +79,7 @@ export class Designable { if (!this.events[name]) { return; } - this.events[name].forEach((fn) => fn.bind(this)(...args)); + this.events[name].forEach((fn) => fn.bind(this)(this.current, ...args)); } insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) { @@ -92,17 +95,16 @@ export class Designable { } } - removeIfChildrenEmpty(schema?: Schema) { + removeIfNoChildren(schema?: Schema) { if (!schema) { return; } let s = schema; const count = Object.keys(s.properties || {}).length; - console.log('removeIfChildrenEmpty', count, s.properties); if (count > 0) { return; } - let removed; + let removed: Schema; while (s.parent) { removed = s.parent.removeProperty(s.name); const count = Object.keys(s.parent.properties || {}).length; @@ -111,18 +113,42 @@ export class Designable { } s = s.parent; } + return removed; } remove(schema?: Schema, options: RemoveOptions = {}) { - const { removeEmptyParents, breakComponent } = options; + const { removeParentsIfNoChildren, breakSchema } = options; let s = schema || this.current; let removed; + const matchSchema = (source: ISchema, target: ISchema) => { + if (!source || !target) { + return; + } + for (const key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + const value = target[key]; + if (value !== source?.[key]) { + return false; + } + } + } + return true; + }; while (s.parent) { removed = s.parent.removeProperty(s.name); - if (!removeEmptyParents) { + if (!removeParentsIfNoChildren) { break; } - if (s?.parent?.['x-component'] === breakComponent) { + if (typeof breakSchema === 'function') { + if (breakSchema(s?.parent)) { + break; + } + } else { + if (matchSchema(s?.parent, breakSchema)) { + break; + } + } + if (s?.parent?.['x-component'] === breakSchema) { break; } const count = Object.keys(s.parent.properties || {}).length; @@ -131,7 +157,7 @@ export class Designable { } s = s.parent; } - this.emit('afterRemove', removed); + this.emit('afterRemove', removed, options); } insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) { @@ -164,33 +190,38 @@ export class Designable { if (!Schema.isSchemaInstance(this.current)) { return; } - const { wrap = defaultWrap, removeEmptyParents } = options; - const properties = {}; - let start = false; - + const opts = {}; + const { wrap = defaultWrap, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { schema.parent.removeProperty(schema.name); - if (removeEmptyParents) { - this.removeIfChildrenEmpty(schema.parent); + if (removeParentsIfNoChildren) { + opts['removed'] = this.removeIfNoChildren(schema.parent); } } - + const properties = {}; + let start = false; + let order = 0; + let newOrder = 0; this.current.parent.mapProperties((property, key) => { if (key === this.current.name) { + newOrder = order; start = true; + ++order; } + property['x-index'] = order; + ++order; if (start) { properties[key] = property; this.current.parent.removeProperty(key); } }); - this.prepareProperty(schema); const wrapped = wrap(schema); - const s = this.current.parent.addProperty(wrapped.name, wrapped); + const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped); + s['x-index'] = newOrder; s.parent = this.current.parent; this.current.parent.setProperties(properties); - this.emit('afterInsertAdjacent', 'beforeBegin', s); + this.emit('afterInsertAdjacent', 'beforeBegin', s, opts); } /** @@ -203,24 +234,29 @@ export class Designable { if (!Schema.isSchemaInstance(this.current)) { return; } - const { wrap = defaultWrap, removeEmptyParents } = options; + const opts = {}; + const { wrap = defaultWrap, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { schema.parent.removeProperty(schema.name); - if (removeEmptyParents) { - this.removeIfChildrenEmpty(schema.parent); + if (removeParentsIfNoChildren) { + opts['removed'] = this.removeIfNoChildren(schema.parent); } } const properties = {}; - this.current.mapProperties((schema, key) => { - properties[key] = schema; + let order = 1; + this.current.mapProperties((s, key) => { + s['x-index'] = order; + ++order; + properties[key] = s; }); this.current.properties = {}; this.prepareProperty(schema); const wrapped = wrap(schema); - const s = this.current.addProperty(wrapped.name, wrapped); + const s = this.current.addProperty(wrapped.name || uid(), wrapped); + s['x-index'] = 0; s.parent = this.current; this.current.setProperties(properties); - this.emit('afterInsertAdjacent', 'afterBegin', s); + this.emit('afterInsertAdjacent', 'afterBegin', s, opts); } /** @@ -233,18 +269,19 @@ export class Designable { if (!Schema.isSchemaInstance(this.current)) { return; } - const { wrap = defaultWrap, removeEmptyParents } = options; + const opts = {}; + const { wrap = defaultWrap, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { schema.parent.removeProperty(schema.name); - if (removeEmptyParents) { - this.removeIfChildrenEmpty(schema.parent); + if (removeParentsIfNoChildren) { + opts['removed'] = this.removeIfNoChildren(schema.parent); } } this.prepareProperty(schema); const wrapped = wrap(schema); const s = this.current.addProperty(wrapped.name || uid(), wrapped); s.parent = this.current; - this.emit('afterInsertAdjacent', 'beforeEnd', s); + this.emit('afterInsertAdjacent', 'beforeEnd', s, opts); } /** @@ -254,22 +291,29 @@ export class Designable { if (!Schema.isSchemaInstance(this.current)) { return; } - const properties = {}; - let start = false; - const { wrap = defaultWrap, removeEmptyParents } = options; + const opts = {}; + const { wrap = defaultWrap, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { schema.parent.removeProperty(schema.name); - console.log('insertAfterEnd', removeEmptyParents); - if (removeEmptyParents) { - this.removeIfChildrenEmpty(schema.parent); + if (removeParentsIfNoChildren) { + opts['removed'] = this.removeIfNoChildren(schema.parent); } schema.parent = null; } + let order = 0; + let newOrder = 0; + let start = false; + const properties = {}; + this.current.parent.mapProperties((property, key) => { + property['x-index'] = order; if (key === this.current.name) { + ++order; + newOrder = order; start = true; } + ++order; if (start && key !== this.current.name) { properties[key] = property; this.current.parent.removeProperty(key); @@ -277,13 +321,12 @@ export class Designable { }); this.prepareProperty(schema); - const wrapped = wrap(schema); - const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped); s.parent = this.current.parent; + s['x-index'] = newOrder; this.current.parent.setProperties(properties); - this.emit('afterInsertAdjacent', 'afterEnd', s); + this.emit('afterInsertAdjacent', 'afterEnd', s, opts); } } @@ -297,7 +340,16 @@ export function useDesignable() { const field = useField(); const fieldSchema = useFieldSchema(); const dn = createDesignable({ current: fieldSchema }); - dn.on('afterInsertAdjacent', refresh); + const api = useAPIClient(); + dn.on('afterInsertAdjacent', async (current, position, schema) => { + refresh(); + // await api.request({ + // url: `/ui_schemas:insertAdjacent/${current['x-uid']}?position=${position}`, + // method: 'post', + // data: schema.toJSON(), + // }); + // console.log(current, position, schema); + }); dn.on('afterRemove', refresh); return { designable,