diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx index 27e77f818..fc8a36442 100644 --- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx +++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/css'; import { observer, RecursionField, useFieldSchema } from '@formily/react'; import { Space } from 'antd'; import React from 'react'; @@ -25,8 +26,15 @@ export const ActionBar = observer((props: any) => { ); } return ( -
-
+
+
{fieldSchema.mapProperties((schema, key) => { diff --git a/packages/core/client/src/schema-component/antd/grid/Grid.tsx b/packages/core/client/src/schema-component/antd/grid/Grid.tsx index 710bd0500..6c36dc383 100644 --- a/packages/core/client/src/schema-component/antd/grid/Grid.tsx +++ b/packages/core/client/src/schema-component/antd/grid/Grid.tsx @@ -1,14 +1,14 @@ -import { useDndContext, useDndMonitor, useDroppable } from '@dnd-kit/core'; +import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core'; import { css } from '@emotion/css'; import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared'; import cls from 'classnames'; import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; -import { useFormBlockContext, useSchemaInitializer } from '../../../'; +import { useDesignable, useFormBlockContext, useSchemaInitializer } from '../../../'; import { DndContext } from '../../common/dnd-context'; -const GridRowContext = createContext(null); -const GridColContext = createContext(null); +const GridRowContext = createContext({}); +const GridColContext = createContext({}); const GridContext = createContext({}); const breakRemoveOnGrid = (s: Schema) => s['x-component'] === 'Grid'; @@ -19,6 +19,8 @@ const ColDivider = (props) => { id: props.id, data: props.data, }); + const { dn, designable } = useDesignable(); + const dividerRef = useRef(); const droppableStyle = { backgroundColor: isOver ? 'rgba(241, 139, 98, .1)' : undefined, @@ -38,10 +40,89 @@ const ColDivider = (props) => { visible = activeSchema !== currentSchema && downSchema !== activeSchema; } } + const prevSchema = props.cols[props.index]; + const nextSchema = props.cols[props.index + 1]; + const { + attributes, + listeners, + setNodeRef: setDraggableNodeRef, + isDragging, + } = useDraggable({ + disabled: props.first || props.last || !designable, + id: props.id, + data: { + dividerRef, + prevSchema, + nextSchema, + }, + }); + + const [clientWidths, setClientWidths] = useState([0, 0]); + + useDndMonitor({ + onDragStart(event) { + if (!isDragging) { + return; + } + const el = dividerRef.current; + const prev = el.previousElementSibling as HTMLDivElement; + const next = el.nextElementSibling as HTMLDivElement; + setClientWidths([prev.clientWidth, next.clientWidth]); + }, + onDragMove(event) { + if (!isDragging) { + return; + } + const el = dividerRef.current; + const prev = el.previousElementSibling as HTMLDivElement; + const next = el.nextElementSibling as HTMLDivElement; + prev.style.width = `calc(${clientWidths[0]}px + ${event.delta.x}px)`; + next.style.width = `calc(${clientWidths[1]}px - ${event.delta.x}px)`; + }, + onDragEnd(event) { + if (clientWidths[0] <= 0 || clientWidths[1] <= 0) { + return; + } + setClientWidths([0, 0]); + if (!prevSchema || !nextSchema) { + return; + } + const el = dividerRef.current; + const prev = el.previousElementSibling as HTMLDivElement; + const next = el.nextElementSibling as HTMLDivElement; + prevSchema['x-component-props'] = prevSchema['x-component-props'] || {}; + nextSchema['x-component-props'] = nextSchema['x-component-props'] || {}; + prevSchema['x-component-props']['width'] = + (100 * (prev?.clientWidth + 24 + 24 / props.cols.length)) / el.parentElement.clientWidth; + nextSchema['x-component-props']['width'] = + (100 * (next?.clientWidth + 24 + 24 / props.cols.length)) / el.parentElement.clientWidth; + dn.emit('batchPatch', { + schemas: [ + { + ['x-uid']: prevSchema['x-uid'], + 'x-component-props': { + ...prevSchema['x-component-props'], + }, + }, + { + ['x-uid']: nextSchema['x-uid'], + 'x-component-props': { + ...nextSchema['x-component-props'], + }, + }, + ], + }); + }, + }); return (
{ + if (visible) { + setNodeRef(el); + dividerRef.current = el; + } + }} className={cls( 'nb-col-divider', css` @@ -49,7 +130,36 @@ const ColDivider = (props) => { `, )} style={{ ...droppableStyle }} - >
+ > +
+
); }; @@ -255,7 +365,7 @@ Grid.Row = observer((props) => { const cols = useColProperties(); return ( - +
{ { Grid.Col = observer((props: any) => { const { cols } = useContext(GridRowContext); - const w = props.width || 100 / cols.length; + const schema = useFieldSchema(); + const field = useField(); + const w = schema?.['x-component-props']?.['width'] || 100 / cols.length; const width = `calc(${w}% - 24px - 24px / ${cols.length})`; + const { isOver, setNodeRef } = useDroppable({ + id: field.address.toString(), + data: { + insertAdjacent: 'beforeEnd', + schema, + wrapSchema: (s) => s, + }, + }); return ( -
- {props.children} -
+ +
+ {props.children} +
+
); }); diff --git a/packages/core/client/src/schema-component/common/dnd-context/index.tsx b/packages/core/client/src/schema-component/common/dnd-context/index.tsx index 8f6013b9a..acf9a80bf 100644 --- a/packages/core/client/src/schema-component/common/dnd-context/index.tsx +++ b/packages/core/client/src/schema-component/common/dnd-context/index.tsx @@ -1,7 +1,7 @@ import { DndContext as DndKitContext, DragEndEvent, DragOverlay, rectIntersection } from '@dnd-kit/core'; import { Props } from '@dnd-kit/core/dist/components/DndContext/DndContext'; import { observer } from '@formily/react'; -import React from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient } from '../../../'; import { createDesignable, useDesignable } from '../../hooks'; @@ -18,6 +18,7 @@ const useDragEnd = (props?: any) => { const insertAdjacent = over?.data?.current?.insertAdjacent; const breakRemoveOn = over?.data?.current?.breakRemoveOn; const wrapSchema = over?.data?.current?.wrapSchema; + const onSuccess = over?.data?.current?.onSuccess; if (!activeSchema || !overSchema) { props?.onDragEnd?.(event); @@ -49,6 +50,7 @@ const useDragEnd = (props?: any) => { wrap: wrapSchema, breakRemoveOn, removeParentsIfNoChildren: true, + onSuccess, }); props?.onDragEnd?.(event); return; @@ -58,15 +60,25 @@ const useDragEnd = (props?: any) => { export const DndContext = observer((props: Props) => { const { t } = useTranslation(); + const [visible, setVisible] = useState(true); return ( - + { + const { active } = event; + const activeSchema = active?.data?.current?.schema; + setVisible(!!activeSchema); + }} + onDragEnd={useDragEnd(props)} + > - {t('Dragging')} + {visible && {t('Dragging')}} {props.children} diff --git a/packages/core/client/src/schema-component/hooks/useDesignable.tsx b/packages/core/client/src/schema-component/hooks/useDesignable.tsx index b989cb8b5..a51c98540 100644 --- a/packages/core/client/src/schema-component/hooks/useDesignable.tsx +++ b/packages/core/client/src/schema-component/hooks/useDesignable.tsx @@ -111,7 +111,35 @@ export class Designable { if (!api) { return; } - this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, removed }) => { + const updateColumnSize = (parent: Schema) => { + if (!parent) { + return; + } + const len = Object.values(parent.properties).length; + const schemas = []; + parent.mapProperties((s) => { + s['x-component-props'] = s['x-component-props'] || {}; + s['x-component-props']['width'] = 100 / len; + if (s['x-uid']) { + schemas.push({ + 'x-uid': s['x-uid'], + 'x-component-props': s['x-component-props'], + }); + } + }); + if (parent['x-uid'] && schemas.length) { + return schemas; + } + return []; + }; + this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, wrapped, removed }) => { + let schemas = []; + if (wrapped?.['x-component'] === 'Grid.Col') { + schemas = updateColumnSize(wrapped.parent); + } + if (removed?.['x-component'] === 'Grid.Col') { + schemas = updateColumnSize(removed.parent); + } refresh(); if (!current['x-uid']) { return; @@ -124,6 +152,13 @@ export class Designable { wrap, }, }); + if (schemas.length) { + await api.request({ + url: `/uiSchemas:batchPatch`, + method: 'post', + data: schemas, + }); + } if (removed?.['x-uid']) { await api.request({ url: `/uiSchemas:remove/${removed['x-uid']}`, @@ -147,7 +182,20 @@ export class Designable { }); message.success(t('Saved successfully'), 0.2); }); + this.on('batchPatch', async ({ schemas }) => { + refresh(); + await api.request({ + url: `/uiSchemas:batchPatch`, + method: 'post', + data: schemas, + }); + message.success(t('Saved successfully'), 0.2); + }); this.on('remove', async ({ removed }) => { + let schemas = []; + if (removed?.['x-component'] === 'Grid.Col') { + schemas = updateColumnSize(removed.parent); + } refresh(); if (!removed?.['x-uid']) { return; @@ -156,6 +204,13 @@ export class Designable { url: `/uiSchemas:remove/${removed['x-uid']}`, method: 'post', }); + if (schemas.length) { + await api.request({ + url: `/uiSchemas:batchPatch`, + method: 'post', + data: schemas, + }); + } message.success(t('Saved successfully'), 0.2); }); } @@ -177,14 +232,14 @@ export class Designable { generateUid(schema); } - on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch', listener: any) { + on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', listener: any) { if (!this.events[name]) { this.events[name] = []; } this.events[name].push(listener); } - emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch', ...args) { + emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', ...args) { if (!this.events[name]) { return; } @@ -356,6 +411,7 @@ export class Designable { this.emit('insertAdjacent', { position: 'beforeBegin', schema: schema2, + wrapped, wrap: schema1, ...opts, }); @@ -407,6 +463,7 @@ export class Designable { position: 'afterBegin', schema: schema2, wrap: schema1, + wrapped, ...opts, }); } @@ -421,6 +478,7 @@ export class Designable { if (!Schema.isSchemaInstance(this.current)) { return; } + delete schema['x-index']; const opts = { onSuccess: options.onSuccess }; const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { @@ -447,6 +505,7 @@ export class Designable { position: 'beforeEnd', schema: schema2, wrap: schema1, + wrapped, ...opts, }); } @@ -508,6 +567,7 @@ export class Designable { position: 'afterEnd', schema: schema2, wrap: schema1, + wrapped, ...opts, }); } diff --git a/packages/plugins/ui-schema-storage/src/actions/ui-schema-action.ts b/packages/plugins/ui-schema-storage/src/actions/ui-schema-action.ts index e21290d67..8cc649dc6 100644 --- a/packages/plugins/ui-schema-storage/src/actions/ui-schema-action.ts +++ b/packages/plugins/ui-schema-storage/src/actions/ui-schema-action.ts @@ -43,6 +43,7 @@ export const uiSchemaActions = { insertNewSchema: callRepositoryMethod('insertNewSchema', 'values'), remove: callRepositoryMethod('remove', 'resourceIndex'), patch: callRepositoryMethod('patch', 'values'), + batchPatch: callRepositoryMethod('batchPatch', 'values'), clearAncestor: callRepositoryMethod('clearAncestor', 'resourceIndex'), async insertAdjacent(ctx: Context, next) { diff --git a/packages/plugins/ui-schema-storage/src/repository.ts b/packages/plugins/ui-schema-storage/src/repository.ts index 2807d5ed0..0128497f4 100644 --- a/packages/plugins/ui-schema-storage/src/repository.ts +++ b/packages/plugins/ui-schema-storage/src/repository.ts @@ -303,6 +303,14 @@ export class UiSchemaRepository extends Repository { await traverSchemaTree(newSchema); } + @transaction() + async batchPatch(schemas: any[], options?) { + const { transaction } = options; + for (const schema of schemas) { + await this.patch(schema, { ...options, transaction }); + } + } + async updateNode(uid: string, schema: any, transaction?: Transaction) { const nodeModel = await this.findOne({ filter: {