diff --git a/packages/client/src/schema-component/antd/grid/AddFormItem.tsx b/packages/client/src/schema-component/antd/grid/AddFormItem.tsx index 50a89f00d..08ec048f3 100644 --- a/packages/client/src/schema-component/antd/grid/AddFormItem.tsx +++ b/packages/client/src/schema-component/antd/grid/AddFormItem.tsx @@ -75,7 +75,7 @@ const useCurrentFieldSchema = (path: string) => { schema && remove(schema, { removeParentsIfNoChildren: true, - breakSchema: (s) => { + breakRemoveOn: (s) => { return s['x-component'] === '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 0bc934ced..d46c6b1c4 100644 --- a/packages/client/src/schema-component/common/dnd-context/index.tsx +++ b/packages/client/src/schema-component/common/dnd-context/index.tsx @@ -1,15 +1,18 @@ import { DndContext as DndKitContext, DragEndEvent, rectIntersection } from '@dnd-kit/core'; import { observer } from '@formily/react'; import React from 'react'; +import { useAPIClient } from '../../../'; import { createDesignable, useDesignable } from '../../hooks'; const useDragEnd = () => { const { refresh } = useDesignable(); + const api = useAPIClient(); return ({ active, over }: DragEndEvent) => { const activeSchema = active?.data?.current?.schema; const overSchema = over?.data?.current?.schema; const insertAdjacent = over?.data?.current?.insertAdjacent; + const breakRemoveOn = over?.data?.current?.breakRemoveOn; const wrapSchema = over?.data?.current?.wrapSchema; if (!activeSchema || !overSchema) { @@ -21,11 +24,12 @@ const useDragEnd = () => { } const dn = createDesignable({ + api, + refresh, current: overSchema, }); - dn.on('afterInsertAdjacent', refresh); - dn.on('afterRemove', refresh); + dn.loadAPIClientEvents(); if (activeSchema.parent === overSchema.parent) { return dn.insertBeforeBeginOrAfterEnd(activeSchema); @@ -34,6 +38,7 @@ const useDragEnd = () => { if (insertAdjacent) { dn.insertAdjacent(insertAdjacent, activeSchema, { wrap: wrapSchema, + breakRemoveOn, removeParentsIfNoChildren: true, }); return; diff --git a/packages/client/src/schema-component/hooks/__tests__/designable.test.ts b/packages/client/src/schema-component/hooks/__tests__/designable.test.ts index 032fa5ef2..69f602419 100644 --- a/packages/client/src/schema-component/hooks/__tests__/designable.test.ts +++ b/packages/client/src/schema-component/hooks/__tests__/designable.test.ts @@ -217,14 +217,14 @@ describe('createDesignable', () => { }); }); - describe('removeIfNoChildren', () => { + describe('recursiveRemoveIfNoChildren', () => { test('has child nodes', () => { - dn.removeIfNoChildren(schema.properties.current); + dn.recursiveRemoveIfNoChildren(schema.properties.current); expect(schema.properties.current).toBeDefined(); }); test('no child nodes', () => { - dn.removeIfNoChildren(schema.properties.current.properties.child); + dn.recursiveRemoveIfNoChildren(schema.properties.current.properties.child); expect(schema.properties.current?.properties?.child).toBeUndefined(); }); }); @@ -241,20 +241,20 @@ describe('createDesignable', () => { expect(schema.properties.current?.properties?.child).toBeUndefined(); }); - test('removeParentsIfNoChildren + breakSchema json', () => { + test('removeParentsIfNoChildren + breakRemoveOn json', () => { dn.remove(schema.properties.current.properties.child, { removeParentsIfNoChildren: true, - breakSchema: { + breakRemoveOn: { 'x-uid': 'global', }, }); expect(schema?.properties?.current).toBeUndefined(); }); - test('removeParentsIfNoChildren + breakSchema function', () => { + test('removeParentsIfNoChildren + breakRemoveOn function', () => { dn.remove(schema.properties.current.properties.child, { removeParentsIfNoChildren: true, - breakSchema: (s) => s['x-uid'] === 'global', + breakRemoveOn: (s) => s['x-uid'] === 'global', }); expect(schema?.properties?.current).toBeUndefined(); }); @@ -486,6 +486,6 @@ describe('parentsIn', () => { dn.on('error', callback); dn.insertAfterBegin(schema.properties.menu); expect(schema.properties.menu).toBeDefined(); - expect(callback.mock.calls[0][1].code).toBe('parent_is_not_allowed'); + expect(callback.mock.calls[0][0].code).toBe('parent_is_not_allowed'); }); }); diff --git a/packages/client/src/schema-component/hooks/useDesignable.tsx b/packages/client/src/schema-component/hooks/useDesignable.tsx index 828c463ee..cd1f15543 100644 --- a/packages/client/src/schema-component/hooks/useDesignable.tsx +++ b/packages/client/src/schema-component/hooks/useDesignable.tsx @@ -3,11 +3,13 @@ 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 { APIClient, useAPIClient } from '../../api-client'; import { SchemaComponentContext } from '../context'; interface CreateDesignableProps { current: Schema; + api?: APIClient; + refresh?: () => void; } export function createDesignable(options: CreateDesignableProps) { @@ -22,13 +24,18 @@ type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; interface InsertAdjacentOptions { wrap?: (s: ISchema) => ISchema; removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | BreakFn; } type BreakFn = (s: ISchema) => boolean; interface RemoveOptions { removeParentsIfNoChildren?: boolean; - breakSchema?: ISchema | BreakFn; + breakRemoveOn?: ISchema | BreakFn; +} + +interface RecursiveRemoveOptions { + breakRemoveOn?: ISchema | BreakFn; } const generateUid = (s: ISchema) => { @@ -42,13 +49,59 @@ const generateUid = (s: ISchema) => { const defaultWrap = (s: ISchema) => s; +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; +}; + export class Designable { current: Schema; - + options: CreateDesignableProps; events = {}; - constructor({ current }) { - this.current = current; + constructor(options: CreateDesignableProps) { + this.options = options; + this.current = options.current; + } + + loadAPIClientEvents() { + const { refresh, api } = this.options; + if (!api) { + return; + } + this.on('insertAdjacent', async ({ current, position, schema, removed }) => { + refresh(); + await api.request({ + url: `/ui_schemas:insertAdjacent/${current['x-uid']}?position=${position}`, + method: 'post', + data: schema.toJSON(), + }); + if (removed?.['x-uid']) { + await api.request({ + url: `/ui_schemas:remove/${removed['x-uid']}`, + method: 'post', + }); + } + }); + this.on('remove', async ({ removed }) => { + refresh(); + if (removed?.['x-uid']) { + await api.request({ + url: `/ui_schemas:remove/${removed['x-uid']}`, + method: 'post', + }); + } + }); } prepareProperty(schema: ISchema) { @@ -68,18 +121,19 @@ export class Designable { generateUid(schema); } - on(name: 'afterInsertAdjacent' | 'afterRemove' | 'error', listener: any) { + on(name: 'insertAdjacent' | 'remove' | 'error', listener: any) { if (!this.events[name]) { this.events[name] = []; } this.events[name].push(listener); } - emit(name: 'afterInsertAdjacent' | 'afterRemove' | 'error', ...args) { + emit(name: 'insertAdjacent' | 'remove' | 'error', ...args) { if (!this.events[name]) { return; } - this.events[name].forEach((fn) => fn.bind(this)(this.current, ...args)); + const [opts, ...others] = args; + this.events[name].forEach((fn) => fn.bind(this)({ current: this.current, ...opts }, ...others)); } parentsIn(schema: Schema) { @@ -112,69 +166,46 @@ export class Designable { } } - removeIfNoChildren(schema?: Schema) { + recursiveRemoveIfNoChildren(schema?: Schema, options?: RecursiveRemoveOptions) { if (!schema) { return; } let s = schema; - const count = Object.keys(s.properties || {}).length; - if (count > 0) { - return; - } let removed: Schema; - while (s.parent) { - removed = s.parent.removeProperty(s.name); - const count = Object.keys(s.parent.properties || {}).length; + const breakRemoveOn = options?.breakRemoveOn; + while (s) { + if (typeof breakRemoveOn === 'function') { + if (breakRemoveOn(s)) { + break; + } + } else { + if (matchSchema(s, breakRemoveOn)) { + break; + } + } + const count = Object.keys(s.properties || {}).length; if (count > 0) { break; } + if (s.parent) { + removed = s.parent.removeProperty(s.name); + } s = s.parent; } return removed; } remove(schema?: Schema, options: RemoveOptions = {}) { - const { removeParentsIfNoChildren, breakSchema } = options; + const { breakRemoveOn, removeParentsIfNoChildren } = options; let s = schema || this.current; - let removed; - const matchSchema = (source: ISchema, target: ISchema) => { - if (!source || !target) { - return; + let removed = s.parent.removeProperty(s.name); + if (removeParentsIfNoChildren) { + const parent = this.recursiveRemoveIfNoChildren(s.parent, { breakRemoveOn }); + if (parent) { + removed = parent; } - 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 (!removeParentsIfNoChildren) { - break; - } - 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; - if (count > 0) { - break; - } - s = s.parent; } - this.emit('afterRemove', removed, options); + this.emit('remove', { removed }); } insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) { @@ -208,7 +239,7 @@ export class Designable { return; } const opts = {}; - const { wrap = defaultWrap, removeParentsIfNoChildren } = options; + const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { @@ -219,7 +250,7 @@ export class Designable { } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { - opts['removed'] = this.removeIfNoChildren(schema.parent); + opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } const properties = {}; @@ -245,7 +276,11 @@ export class Designable { s['x-index'] = newOrder; s.parent = this.current.parent; this.current.parent.setProperties(properties); - this.emit('afterInsertAdjacent', 'beforeBegin', s, opts); + this.emit('insertAdjacent', { + position: 'beforeBegin', + schema: s, + ...opts, + }); } /** @@ -259,7 +294,7 @@ export class Designable { return; } const opts = {}; - const { wrap = defaultWrap, removeParentsIfNoChildren } = options; + const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { @@ -270,7 +305,7 @@ export class Designable { } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { - opts['removed'] = this.removeIfNoChildren(schema.parent); + opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } const properties = {}; @@ -287,7 +322,11 @@ export class Designable { s['x-index'] = 0; s.parent = this.current; this.current.setProperties(properties); - this.emit('afterInsertAdjacent', 'afterBegin', s, opts); + this.emit('insertAdjacent', { + position: 'afterBegin', + schema: s, + ...opts, + }); } /** @@ -301,7 +340,7 @@ export class Designable { return; } const opts = {}; - const { wrap = defaultWrap, removeParentsIfNoChildren } = options; + const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { @@ -312,14 +351,18 @@ export class Designable { } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { - opts['removed'] = this.removeIfNoChildren(schema.parent); + opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } 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, opts); + this.emit('insertAdjacent', { + position: 'beforeEnd', + schema: s, + ...opts, + }); } /** @@ -330,7 +373,7 @@ export class Designable { return; } const opts = {}; - const { wrap = defaultWrap, removeParentsIfNoChildren } = options; + const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { @@ -341,7 +384,7 @@ export class Designable { } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { - opts['removed'] = this.removeIfNoChildren(schema.parent); + opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } schema.parent = null; } @@ -371,7 +414,11 @@ export class Designable { s.parent = this.current.parent; s['x-index'] = newOrder; this.current.parent.setProperties(properties); - this.emit('afterInsertAdjacent', 'afterEnd', s, opts); + this.emit('insertAdjacent', { + position: 'afterEnd', + schema: s, + ...opts, + }); } } @@ -384,18 +431,9 @@ export function useDesignable() { }; const field = useField(); const fieldSchema = useFieldSchema(); - const dn = createDesignable({ current: fieldSchema }); 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); + const dn = createDesignable({ api, refresh, current: fieldSchema }); + dn.loadAPIClientEvents(); return { designable, reset,