improve Table Operation

This commit is contained in:
chenos 2021-08-03 00:01:42 +08:00
parent 6ff216be6b
commit 754be7d1d1
5 changed files with 313 additions and 85 deletions

View File

@ -13,15 +13,21 @@ import {
} from '@formily/react';
import { Button, Dropdown, Menu, Popover, Space, Drawer, Modal } from 'antd';
import { Link, useHistory, LinkProps } from 'react-router-dom';
import { useDesignable, useDefaultAction } from '..';
import { useDesignable, useDefaultAction, updateSchema } from '..';
import './style.less';
import { uid } from '@formily/shared';
import cls from 'classnames';
import { MenuOutlined } from '@ant-design/icons';
import { FormLayout } from '@formily/antd';
import IconPicker from '../../components/icon-picker';
import { getSchemaPath, useSchemaComponent } from '../../components/schema-renderer';
import {
findPropertyByPath,
getSchemaPath,
useSchemaComponent,
} from '../../components/schema-renderer';
import { VisibleContext } from '../../context';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { createPortal } from 'react-dom';
export const ButtonComponentContext = createContext(null);
@ -192,9 +198,49 @@ Action.Drawer = observer((props: any) => {
});
Action.Dropdown = observer((props: any) => {
const { schema } = useDesignable();
const button = useContext(ButtonComponentContext);
const { root, schema, insertAfter, remove } = useDesignable();
const moveToAfter = (path1, path2) => {
if (!path1 || !path2) {
return;
}
if (path1.join('.') === path2.join('.')) {
return;
}
const data = findPropertyByPath(root, path1);
if (!data) {
return;
}
remove(path1);
return insertAfter(data.toJSON(), path2);
};
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<DndContext
onDragStart={(event) => {
console.log({ event });
setDragOverlayContent(event.active.data?.current?.title || '');
}}
onDragEnd={async (event) => {
const path1 = event.active?.data?.current?.path;
const path2 = event.over?.data?.current?.path;
const data = moveToAfter(path1, path2);
await updateSchema(data);
}}
>
{createPortal(
<DragOverlay
zIndex={2222}
style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}
dropAnimation={{
duration: 10,
easing: 'cubic-bezier(0.18, 0.67, 0.6, 1.22)',
}}
>
{dragOverlayContent}
</DragOverlay>,
document.body,
)}
<Dropdown
trigger={['click']}
{...props}
@ -206,6 +252,7 @@ Action.Dropdown = observer((props: any) => {
>
<span>{button}</span>
</Dropdown>
</DndContext>
);
});

View File

@ -217,17 +217,16 @@ function generateCardItemSchema(component) {
[uid()]: {
type: 'void',
title: '操作',
'x-component': 'Table.Column',
'x-component': 'Table.Operation',
'x-component-props': {
className: 'nb-table-operation',
},
'x-designable-bar': 'Table.OperationDesignableBar',
'x-designable-bar': 'Table.Operation.DesignableBar',
properties: {
[uid()]: {
type: 'void',
name: 'action1',
'x-component': 'Action',
// 'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
icon: 'EllipsisOutlined',
},
@ -254,6 +253,7 @@ function generateCardItemSchema(component) {
title: '查看',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'view',
properties: {
[uid()]: {
type: 'void',
@ -274,10 +274,10 @@ function generateCardItemSchema(component) {
[uid()]: {
type: 'void',
title: '详情',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-designable-bar':
'Tabs.TabPane.DesignableBar',
'x-component': 'Tabs.TabPane',
'x-component-props': {
},
'x-component-props': {},
properties: {
[uid()]: {
type: 'void',
@ -299,6 +299,7 @@ function generateCardItemSchema(component) {
title: '编辑',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'update',
properties: {
[uid()]: {
type: 'void',
@ -328,6 +329,8 @@ function generateCardItemSchema(component) {
type: 'void',
title: '删除',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'destroy',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},

View File

@ -121,7 +121,7 @@ export const Menu: any = observer((props: any) => {
}
remove(path1);
return insertAfter(data.toJSON(), path2);
}
};
const fieldSchema = useFieldSchema();
console.log('Menu.schema', schema, fieldSchema);
const [selectedKey, setSelectedKey] = useState(
@ -325,9 +325,17 @@ Menu.Action = observer((props: any) => {
await run();
setVisible(true);
}}
>
<SortableItem
id={schema.name}
data={{
title: schema.title,
path: getSchemaPath(schema),
}}
>
{schema.title}
<DesignableBar />
</SortableItem>
</AntdMenu.Item>
{props.children}
{/* <RecursionField schema={schema} onlyRenderProperties /> */}

View File

@ -36,6 +36,7 @@ import {
} from '../../components/schema-renderer';
import { updateSchema } from '..';
import { Schema } from '@formily/react';
import { isColumn, isColumnComponent } from '.';
export const RowDraggableContext = createContext({});
export const ColDraggableContext = createContext(null);
@ -188,12 +189,15 @@ export function SortableHeaderRow(props) {
export function SortableHeaderCell(props) {
const id = useContext(CellContext);
if (['RC_TABLE_KEY', 'addnew'].includes(id)) {
if (['RC_TABLE_KEY', 'addnew', 'operation'].includes(id)) {
return <th {...props} />;
}
const { schema } = useDesignable();
const columns: Schema[] = schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Table.Column' && current.name === id) {
if (!isColumn(current)) {
return [...columns];
}
if (current.name === id) {
return [...columns, current];
}
return [...columns];

View File

@ -59,45 +59,7 @@ import {
SortableRowHandle,
} from './Sortable';
import { DragHandle, Droppable, SortableItem } from '../../components/Sortable';
const SyntheticListenerMapContext = createContext(null);
function DragableBodyRow(props: any) {
const {
className,
style: prevStyle,
['data-row-key']: dataRowKey,
...others
} = props;
const {
attributes,
listeners,
setNodeRef,
overIndex,
transform,
transition,
} = useSortable({ id: dataRowKey });
const style = {
...prevStyle,
transform: CSS.Transform.toString(transform),
transition,
};
return (
<SyntheticListenerMapContext.Provider value={listeners}>
<tr
className={cls(className)}
ref={setNodeRef}
{...others}
{...attributes}
style={style}
>
{props.children}
</tr>
</SyntheticListenerMapContext.Provider>
);
}
import { VisibleContext } from '../../context';
export interface ITableContext {
props: any;
@ -265,6 +227,68 @@ const useTableActionBars = () => {
return bars;
};
export function isOperationColumn(schema: Schema) {
return ['Table.Operation'].includes(schema['x-component']);
}
export function isColumn(schema: Schema) {
return ['Table.Column'].includes(schema['x-component']);
}
export function isColumnComponent(component: string) {
return ['Table.Operation', 'Table.Column'].includes(component);
}
const useTableOperation = () => {
const {
field,
schema,
props: { rowKey },
} = useTable();
const findActionDropdown = (schema: Schema) => {
return schema.reduceProperties((s, current) => {
if (current['x-component'] === 'Action.Dropdown') {
return current;
}
const action = findActionDropdown(current);
if (action) {
return action;
}
return s;
}, new Schema({}));
};
const operationSchema = schema.reduceProperties((s, current) => {
if (isOperationColumn(current)) {
return current;
}
return s;
}, new Schema({}));
const dropdownSchema = findActionDropdown(operationSchema);
const columnProps = operationSchema?.['x-component-props'] || {};
return {
title: <Table.Operation path={getSchemaPath(dropdownSchema)} />,
dataIndex: 'operation',
...columnProps,
render: (_: any, record: any) => {
const index = findIndex(
field.value,
(item) => item[rowKey] === record[rowKey],
);
return (
<div className={'nb-table-column'}>
<TableRowContext.Provider value={{ index, record }}>
<RecursionField
schema={operationSchema}
name={index}
onlyRenderProperties
/>
</TableRowContext.Provider>
</div>
);
},
};
};
const useTableColumns = () => {
const {
field,
@ -275,19 +299,21 @@ const useTableColumns = () => {
const { getField } = useCollectionContext();
const columns = schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Table.Column') {
const operation = useTableOperation();
const columnSchemas = schema.reduceProperties((columns, current) => {
if (isColumn(current)) {
return [...columns, current];
}
return [...columns];
}, []);
return columns
.map((column: Schema) => {
const columns: any[] = [operation].concat(
columnSchemas.map((column: Schema) => {
const columnProps = column['x-component-props'] || {};
const collectionField = getField(columnProps.fieldName);
return {
title: () => (
title: (
<CollectionFieldContext.Provider value={collectionField}>
<RecursionField name={column.name} schema={column} onlyRenderSelf />
</CollectionFieldContext.Provider>
@ -308,17 +334,16 @@ const useTableColumns = () => {
);
},
};
})
.concat(
designable
? [
{
}),
);
if (designable) {
columns.push({
title: <AddColumn />,
dataIndex: 'addnew',
},
]
: [],
);
});
}
return columns;
};
function AddColumn() {
@ -593,6 +618,7 @@ const TableProvider = (props: any) => {
};
export const Table: any = observer((props: any) => {
const [visible, setVisible] = useState(false);
return (
<CollectionProvider collectionName={props.collectionName}>
<DisplayedMapProvider>
@ -712,17 +738,117 @@ function generateActionSchema(type) {
'x-decorator-props': {
displayName: 'destroy',
},
'x-action-type': 'destroy',
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
view: {},
update: {},
};
return actions[type];
}
function generateMenuActionSchema(type) {
const actions: { [key: string]: ISchema } = {
view: {
key: uid(),
name: uid(),
type: 'void',
title: '查看',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'view',
properties: {
[uid()]: {
type: 'void',
title: '查看',
'x-component': 'Action.Modal',
'x-component-props': {
bodyStyle: {
background: '#f0f2f5',
// paddingTop: 0,
},
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Tabs',
'x-designable-bar': 'Tabs.DesignableBar',
properties: {
[uid()]: {
type: 'void',
title: '详情',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component': 'Tabs.TabPane',
'x-component-props': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.PaneItem',
},
},
},
},
},
},
},
},
},
},
update: {
key: uid(),
name: uid(),
type: 'void',
title: '编辑',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'update',
properties: {
[uid()]: {
type: 'void',
title: '编辑数据',
'x-decorator': 'Form',
'x-decorator-props': {
useResource: '{{ Table.useResource }}',
useValues: '{{ Table.useTableRowRecord }}',
},
'x-component': 'Action.Modal',
'x-component-props': {
useOkAction: '{{ Table.useTableUpdateAction }}',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},
},
destroy: {
key: uid(),
name: uid(),
type: 'void',
title: '删除',
'x-component': 'Menu.Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'destroy',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
};
return actions[type];
}
function AddActionButton() {
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
@ -1039,12 +1165,36 @@ Table.Filter.DesignableBar = () => {
);
};
Table.OperationDesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
Table.Operation = observer((props: any) => {
const [visible, setVisible] = useState(false);
const isPopup = Object.keys(schema.properties || {}).length > 0;
const inActionBar = schema.parent['x-component'] === 'Table.ActionBar';
return (
<div className={'nb-table-column'}>
<Table.Operation.DesignableBar path={props.path} />
</div>
);
});
Table.Operation.Cell = observer((props: any) => {
const ctx = useContext(TableRowContext);
const schema = props.schema;
return (
<div className={'nb-table-column'}>
<RecursionField schema={schema} name={ctx.index} onlyRenderProperties />
</div>
);
});
Table.Operation.DesignableBar = (props) => {
const { schema, remove, refresh, appendChild } = useDesignable(props.path);
const [visible, setVisible] = useState(false);
const map = new Map();
schema.mapProperties((s) => {
if (!s['x-action-type']) {
return;
}
map.set(s['x-action-type'], s.name);
});
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -1071,7 +1221,20 @@ Table.OperationDesignableBar = () => {
<SwitchMenuItem
key={item.name}
title={item.title}
onChange={async (checked) => {}}
checked={map.has(item.name)}
onChange={async (checked) => {
if (checked) {
const s = generateMenuActionSchema(item.name);
const data = appendChild(s);
await createSchema(data);
} else if (map.get(item.name)) {
const removed = remove([
...props.path,
map.get(item.name),
]);
await removeSchema(removed);
}
}}
/>
))}
</Menu.ItemGroup>
@ -1174,6 +1337,9 @@ Table.Cell = observer((props: any) => {
const ctx = useContext(TableRowContext);
const schema = props.schema;
const collectionField = useContext(CollectionFieldContext);
if (schema['x-component'] === 'Table.Operation') {
return <Table.Operation.Cell {...props} />;
}
return (
<RecursionField
schema={