feat(client): add designable test cases

This commit is contained in:
chenos 2022-02-14 15:50:19 +08:00
parent be489996c3
commit 1a7f638f85
11 changed files with 839 additions and 140 deletions

View File

@ -19,6 +19,7 @@ export interface IResource {
create?: (params?: ActionParams) => Promise<any>; create?: (params?: ActionParams) => Promise<any>;
update?: (params?: ActionParams) => Promise<any>; update?: (params?: ActionParams) => Promise<any>;
destroy?: (params?: ActionParams) => Promise<any>; destroy?: (params?: ActionParams) => Promise<any>;
[key: string]: (params?: ActionParams) => Promise<any>;
} }
export class APIClient { export class APIClient {

View File

@ -103,6 +103,96 @@ const FormBlockInitializerItem = itemWrap((props) => {
); );
}); });
const TestInitializerItem = itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<FormOutlined />}
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
</SchemaInitializer.Item>
);
});
export const AddGridBlockItem = observer((props: any) => { export const AddGridBlockItem = observer((props: any) => {
return ( return (
<SchemaInitializer.Button <SchemaInitializer.Button
@ -123,6 +213,11 @@ export const AddGridBlockItem = observer((props: any) => {
title: 'Form', title: 'Form',
component: FormBlockInitializerItem, component: FormBlockInitializerItem,
}, },
{
type: 'item',
title: 'Test',
component: TestInitializerItem,
},
], ],
}, },
]} ]}

View File

@ -74,8 +74,10 @@ const useCurrentFieldSchema = (path: string) => {
remove() { remove() {
schema && schema &&
remove(schema, { remove(schema, {
breakComponent: 'Grid', removeParentsIfNoChildren: true,
removeEmptyParents: true, breakSchema: (s) => {
return s['x-component'] === 'Grid';
},
}); });
}, },
}; };

View File

@ -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 (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
});

View File

@ -1,5 +1,4 @@
import { observer, useFieldSchema } from '@formily/react'; import { observer, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { BlockItem, DragHandler, Grid, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; import { BlockItem, DragHandler, Grid, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react'; import React from 'react';
@ -13,87 +12,151 @@ const Block = observer((props) => {
); );
}); });
export default function App() { const schema = {
return ( _isJSONSchemaObject: true,
<SchemaComponentProvider components={{ Grid, Block, BlockItem }}> version: '2.0',
<SchemaComponent
schema={{
type: 'void', type: 'void',
name: 'grid1', name: 'grid1',
'x-decorator': 'CardItem',
'x-component': 'Grid', 'x-component': 'Grid',
'x-uid': uid(),
properties: { properties: {
row1: { row1: {
type: 'void', type: 'void',
'x-component': 'Grid.Row', 'x-component': 'Grid.Row',
'x-uid': uid(),
properties: { properties: {
col11: { col11: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
'x-component': 'Grid.Col', 'x-component': 'Grid.Col',
properties: { properties: {
block1: { block1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
title: '1', title: '1',
'x-decorator': 'BlockItem', 'x-decorator': 'BlockItem',
'x-component': 'Block', 'x-component': 'Block',
'x-uid': 'a9m97uffyku',
'x-async': false,
'x-index': 1,
}, },
block2: { block2: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
title: '2', title: '2.1',
'x-decorator': 'BlockItem', 'x-decorator': 'BlockItem',
'x-component': 'Block', '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: { col12: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
'x-component': 'Grid.Col', 'x-component': 'Grid.Col',
properties: { properties: {
block3: { block3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
title: '3', title: '3',
'x-decorator': 'BlockItem', 'x-decorator': 'BlockItem',
'x-component': 'Block', '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: { row2: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
'x-component': 'Grid.Row', 'x-component': 'Grid.Row',
'x-uid': uid(),
properties: { properties: {
col21: { col21: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
'x-component': 'Grid.Col', 'x-component': 'Grid.Col',
properties: { properties: {
block4: { block4: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
title: '4', title: '4',
'x-decorator': 'BlockItem', 'x-decorator': 'BlockItem',
'x-component': 'Block', 'x-component': 'Block',
'x-uid': 'v5bd5kcyhat',
'x-async': false,
'x-index': 1,
}, },
}, },
'x-uid': 'bwg0mv89atk',
'x-async': false,
'x-index': 1,
}, },
col22: { col22: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
'x-component': 'Grid.Col', 'x-component': 'Grid.Col',
properties: { properties: {
block5: { block5: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void', type: 'void',
title: '5', title: '5',
'x-decorator': 'BlockItem', 'x-decorator': 'BlockItem',
'x-component': 'Block', '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 (
<SchemaComponentProvider components={{ Grid, Block, BlockItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider> </SchemaComponentProvider>
); );
} }

View File

@ -1,20 +1,44 @@
import { ISchema } from '@formily/react'; import { ISchema, observer, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared'; 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'; import React from 'react';
const Block = observer((props) => {
const fieldSchema = useFieldSchema();
return (
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
});
const schema: ISchema = { const schema: ISchema = {
type: 'object',
properties: {
grid: {
type: 'void', type: 'void',
name: 'grid1',
'x-component': 'Grid', 'x-component': 'Grid',
'x-item-initializer': 'Grid.AddBlockItem', 'x-item-initializer': 'Grid.AddBlockItem',
'x-uid': uid(), 'x-uid': uid(),
properties: {}, properties: {},
},
},
}; };
export default function App() { export default function App() {
return ( return (
<SchemaComponentProvider components={{ Grid, CardItem, Markdown, Form, VoidTable }}> <SchemaComponentProvider components={{ BlockItem, Block, Grid, CardItem, Markdown, Form, VoidTable }}>
<SchemaComponent schema={schema} /> <SchemaComponent schema={schema} />
</SchemaComponentProvider> </SchemaComponentProvider>
); );

View File

@ -1 +1,3 @@
export * from './Block';
export * from './Grid'; export * from './Grid';

View File

@ -7,7 +7,6 @@ const useDragEnd = () => {
const { refresh } = useDesignable(); const { refresh } = useDesignable();
return ({ active, over }: DragEndEvent) => { return ({ active, over }: DragEndEvent) => {
console.log({ active, over });
const activeSchema = active?.data?.current?.schema; const activeSchema = active?.data?.current?.schema;
const overSchema = over?.data?.current?.schema; const overSchema = over?.data?.current?.schema;
const insertAdjacent = over?.data?.current?.insertAdjacent; const insertAdjacent = over?.data?.current?.insertAdjacent;
@ -35,7 +34,7 @@ const useDragEnd = () => {
if (insertAdjacent) { if (insertAdjacent) {
dn.insertAdjacent(insertAdjacent, activeSchema, { dn.insertAdjacent(insertAdjacent, activeSchema, {
wrap: wrapSchema, wrap: wrapSchema,
removeEmptyParents: true, removeParentsIfNoChildren: true,
}); });
return; return;
} }

View File

@ -34,16 +34,11 @@ export const Sortable = (props: any) => {
export const SortableItem: React.FC<any> = observer((props) => { export const SortableItem: React.FC<any> = observer((props) => {
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const onInsertAdjacent = ({ dn, orginDraggedParentSchema }) => {
dn.removeIfChildrenEmpty(orginDraggedParentSchema, {
removeEmptyParents: true,
});
};
return ( return (
<Sortable <Sortable
{...props} {...props}
id={field.address.toString()} id={field.address.toString()}
data={{ insertAdjacent: 'afterEnd', onInsertAdjacent, schema: fieldSchema }} data={{ insertAdjacent: 'afterEnd', schema: fieldSchema }}
> >
{props.children} {props.children}
</Sortable> </Sortable>

View File

@ -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();
});
});

View File

@ -3,6 +3,7 @@ import { uid } from '@formily/shared';
import get from 'lodash/get'; import get from 'lodash/get';
import set from 'lodash/set'; import set from 'lodash/set';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useAPIClient } from '../../api-client';
import { SchemaComponentContext } from '../context'; import { SchemaComponentContext } from '../context';
interface CreateDesignableProps { interface CreateDesignableProps {
@ -20,12 +21,14 @@ type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
interface InsertAdjacentOptions { interface InsertAdjacentOptions {
wrap?: (s: ISchema) => ISchema; wrap?: (s: ISchema) => ISchema;
removeEmptyParents?: boolean; removeParentsIfNoChildren?: boolean;
} }
type BreakFn = (s: ISchema) => boolean;
interface RemoveOptions { interface RemoveOptions {
removeEmptyParents?: boolean; removeParentsIfNoChildren?: boolean;
breakComponent?: string; breakSchema?: ISchema | BreakFn;
} }
const generateUid = (s: ISchema) => { const generateUid = (s: ISchema) => {
@ -76,7 +79,7 @@ export class Designable {
if (!this.events[name]) { if (!this.events[name]) {
return; 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 = {}) { insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
@ -92,17 +95,16 @@ export class Designable {
} }
} }
removeIfChildrenEmpty(schema?: Schema) { removeIfNoChildren(schema?: Schema) {
if (!schema) { if (!schema) {
return; return;
} }
let s = schema; let s = schema;
const count = Object.keys(s.properties || {}).length; const count = Object.keys(s.properties || {}).length;
console.log('removeIfChildrenEmpty', count, s.properties);
if (count > 0) { if (count > 0) {
return; return;
} }
let removed; let removed: Schema;
while (s.parent) { while (s.parent) {
removed = s.parent.removeProperty(s.name); removed = s.parent.removeProperty(s.name);
const count = Object.keys(s.parent.properties || {}).length; const count = Object.keys(s.parent.properties || {}).length;
@ -111,18 +113,42 @@ export class Designable {
} }
s = s.parent; s = s.parent;
} }
return removed;
} }
remove(schema?: Schema, options: RemoveOptions = {}) { remove(schema?: Schema, options: RemoveOptions = {}) {
const { removeEmptyParents, breakComponent } = options; const { removeParentsIfNoChildren, breakSchema } = options;
let s = schema || this.current; let s = schema || this.current;
let removed; 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) { while (s.parent) {
removed = s.parent.removeProperty(s.name); removed = s.parent.removeProperty(s.name);
if (!removeEmptyParents) { if (!removeParentsIfNoChildren) {
break; 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; break;
} }
const count = Object.keys(s.parent.properties || {}).length; const count = Object.keys(s.parent.properties || {}).length;
@ -131,7 +157,7 @@ export class Designable {
} }
s = s.parent; s = s.parent;
} }
this.emit('afterRemove', removed); this.emit('afterRemove', removed, options);
} }
insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) { insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
@ -164,33 +190,38 @@ export class Designable {
if (!Schema.isSchemaInstance(this.current)) { if (!Schema.isSchemaInstance(this.current)) {
return; return;
} }
const { wrap = defaultWrap, removeEmptyParents } = options; const opts = {};
const properties = {}; const { wrap = defaultWrap, removeParentsIfNoChildren } = options;
let start = false;
if (Schema.isSchemaInstance(schema)) { if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name); schema.parent.removeProperty(schema.name);
if (removeEmptyParents) { if (removeParentsIfNoChildren) {
this.removeIfChildrenEmpty(schema.parent); opts['removed'] = this.removeIfNoChildren(schema.parent);
} }
} }
const properties = {};
let start = false;
let order = 0;
let newOrder = 0;
this.current.parent.mapProperties((property, key) => { this.current.parent.mapProperties((property, key) => {
if (key === this.current.name) { if (key === this.current.name) {
newOrder = order;
start = true; start = true;
++order;
} }
property['x-index'] = order;
++order;
if (start) { if (start) {
properties[key] = property; properties[key] = property;
this.current.parent.removeProperty(key); this.current.parent.removeProperty(key);
} }
}); });
this.prepareProperty(schema); this.prepareProperty(schema);
const wrapped = wrap(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; s.parent = this.current.parent;
this.current.parent.setProperties(properties); 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)) { if (!Schema.isSchemaInstance(this.current)) {
return; return;
} }
const { wrap = defaultWrap, removeEmptyParents } = options; const opts = {};
const { wrap = defaultWrap, removeParentsIfNoChildren } = options;
if (Schema.isSchemaInstance(schema)) { if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name); schema.parent.removeProperty(schema.name);
if (removeEmptyParents) { if (removeParentsIfNoChildren) {
this.removeIfChildrenEmpty(schema.parent); opts['removed'] = this.removeIfNoChildren(schema.parent);
} }
} }
const properties = {}; const properties = {};
this.current.mapProperties((schema, key) => { let order = 1;
properties[key] = schema; this.current.mapProperties((s, key) => {
s['x-index'] = order;
++order;
properties[key] = s;
}); });
this.current.properties = {}; this.current.properties = {};
this.prepareProperty(schema); this.prepareProperty(schema);
const wrapped = wrap(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; s.parent = this.current;
this.current.setProperties(properties); 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)) { if (!Schema.isSchemaInstance(this.current)) {
return; return;
} }
const { wrap = defaultWrap, removeEmptyParents } = options; const opts = {};
const { wrap = defaultWrap, removeParentsIfNoChildren } = options;
if (Schema.isSchemaInstance(schema)) { if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name); schema.parent.removeProperty(schema.name);
if (removeEmptyParents) { if (removeParentsIfNoChildren) {
this.removeIfChildrenEmpty(schema.parent); opts['removed'] = this.removeIfNoChildren(schema.parent);
} }
} }
this.prepareProperty(schema); this.prepareProperty(schema);
const wrapped = wrap(schema); const wrapped = wrap(schema);
const s = this.current.addProperty(wrapped.name || uid(), wrapped); const s = this.current.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current; 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)) { if (!Schema.isSchemaInstance(this.current)) {
return; return;
} }
const properties = {}; const opts = {};
let start = false; const { wrap = defaultWrap, removeParentsIfNoChildren } = options;
const { wrap = defaultWrap, removeEmptyParents } = options;
if (Schema.isSchemaInstance(schema)) { if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name); schema.parent.removeProperty(schema.name);
console.log('insertAfterEnd', removeEmptyParents); if (removeParentsIfNoChildren) {
if (removeEmptyParents) { opts['removed'] = this.removeIfNoChildren(schema.parent);
this.removeIfChildrenEmpty(schema.parent);
} }
schema.parent = null; schema.parent = null;
} }
let order = 0;
let newOrder = 0;
let start = false;
const properties = {};
this.current.parent.mapProperties((property, key) => { this.current.parent.mapProperties((property, key) => {
property['x-index'] = order;
if (key === this.current.name) { if (key === this.current.name) {
++order;
newOrder = order;
start = true; start = true;
} }
++order;
if (start && key !== this.current.name) { if (start && key !== this.current.name) {
properties[key] = property; properties[key] = property;
this.current.parent.removeProperty(key); this.current.parent.removeProperty(key);
@ -277,13 +321,12 @@ export class Designable {
}); });
this.prepareProperty(schema); this.prepareProperty(schema);
const wrapped = wrap(schema); const wrapped = wrap(schema);
const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped); const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current.parent; s.parent = this.current.parent;
s['x-index'] = newOrder;
this.current.parent.setProperties(properties); 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 field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const dn = createDesignable({ current: fieldSchema }); 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); dn.on('afterRemove', refresh);
return { return {
designable, designable,