action bar

This commit is contained in:
chenos 2021-07-30 16:10:40 +08:00
parent 7dd9fbf213
commit c3d2c3cd28
5 changed files with 502 additions and 171 deletions

View File

@ -0,0 +1,40 @@
import React, { useEffect, useState } from 'react';
import { Menu, Switch } from 'antd';
export interface SwitchMenuItemProps {
onChange?: any;
title?: any;
checked?: boolean;
}
export function SwitchMenuItem(props: SwitchMenuItemProps) {
const { onChange } = props;
const [checked, setChecked] = useState(props.checked);
useEffect(() => {
setChecked(props.checked);
}, [props.checked]);
return (
<Menu.Item
style={{ minWidth: 150 }}
onClick={async () => {
setChecked((checked) => {
onChange(!checked);
return !checked;
});
}}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span>{props.title}</span>
<Switch checked={checked} size={'small'} />
</div>
</Menu.Item>
);
}
export default SwitchMenuItem;

View File

@ -69,6 +69,7 @@ import {
useCollectionsContext, useCollectionsContext,
useDisplayedMapContext, useDisplayedMapContext,
} from '../../constate'; } from '../../constate';
import SwitchMenuItem from '../../components/SwitchMenuItem';
const generateGridBlock = (schema: ISchema) => { const generateGridBlock = (schema: ISchema) => {
const name = schema.name || uid(); const name = schema.name || uid();
@ -149,6 +150,12 @@ function generateCardItemSchema(component) {
properties: { properties: {
[uid()]: { [uid()]: {
type: 'void', type: 'void',
title: '筛选',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'filter',
},
'x-align': 'left',
'x-component': 'Table.Filter', 'x-component': 'Table.Filter',
'x-designable-bar': 'Table.Filter.DesignableBar', 'x-designable-bar': 'Table.Filter.DesignableBar',
'x-component-props': { 'x-component-props': {
@ -159,6 +166,11 @@ function generateCardItemSchema(component) {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '新增', title: '新增',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component': 'Action', 'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
properties: { properties: {
@ -186,6 +198,11 @@ function generateCardItemSchema(component) {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '删除', title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action', 'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
'x-component-props': { 'x-component-props': {
@ -201,6 +218,7 @@ function generateCardItemSchema(component) {
'x-component-props': { 'x-component-props': {
className: 'nb-table-operation', className: 'nb-table-operation',
}, },
'x-designable-bar': 'Table.OperationDesignableBar',
properties: { properties: {
[uid()]: { [uid()]: {
type: 'void', type: 'void',
@ -578,32 +596,6 @@ AddNew.CardItem = observer((props: any) => {
); );
}); });
function SwitchField(props) {
const { field, onChange } = props;
const [checked, setChecked] = useState(props.checked);
useEffect(() => {
setChecked(props.checked);
}, [props.checked]);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
onClick={async () => {
setChecked((checked) => {
onChange(!checked);
return !checked;
});
}}
>
<span>{field.uiSchema.title}</span>
<Switch checked={checked} size={'small'} />
</div>
);
}
AddNew.FormItem = observer((props: any) => { AddNew.FormItem = observer((props: any) => {
const { ghost, defaultAction } = props; const { ghost, defaultAction } = props;
const { schema, insertBefore, insertAfter, appendChild, deepRemove } = const { schema, insertBefore, insertAfter, appendChild, deepRemove } =
@ -624,57 +616,56 @@ AddNew.FormItem = observer((props: any) => {
<Menu> <Menu>
<Menu.ItemGroup className={'display-fields'} title={`字段展示`}> <Menu.ItemGroup className={'display-fields'} title={`字段展示`}>
{fields?.map((field) => ( {fields?.map((field) => (
<Menu.Item key={field.key}> <SwitchMenuItem
<SwitchField key={field.key}
field={field} title={field?.uiSchema?.title}
checked={displayed.has(field.name)} checked={displayed.has(field.name)}
onChange={async (checked) => { onChange={async (checked) => {
if (!checked) { if (!checked) {
const s: any = displayed.get(field.name); const s: any = displayed.get(field.name);
const p = getSchemaPath(s); const p = getSchemaPath(s);
const removed = deepRemove(p); const removed = deepRemove(p);
if (!removed) { if (!removed) {
console.log('getSchemaPath', p, removed); console.log('getSchemaPath', p, removed);
return;
}
const last = removed.pop();
displayed.remove(field.name);
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
return; return;
} }
let data: ISchema = { const last = removed.pop();
key: uid(), displayed.remove(field.name);
type: 'void', if (isGridRowOrCol(last)) {
'x-decorator': 'Form.Field.Item', await removeSchema(last);
'x-designable-bar': 'Form.Field.DesignableBar',
'x-component': 'Form.Field',
'x-component-props': {
fieldName: field.name,
},
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
} }
if (data) { return;
let s; }
if (isGrid(schema)) { let data: ISchema = {
s = appendChild(data, [...path]); key: uid(),
} else if (defaultAction === 'insertAfter') { type: 'void',
s = insertAfter(data, [...path]); 'x-decorator': 'Form.Field.Item',
} else { 'x-designable-bar': 'Form.Field.DesignableBar',
s = insertBefore(data, [...path]); 'x-component': 'Form.Field',
} 'x-component-props': {
await createSchema(s); fieldName: field.name,
},
};
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
} }
}} await createSchema(s);
/> }
</Menu.Item> }}
/>
))} ))}
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.Divider /> <Menu.Divider />
@ -968,4 +959,16 @@ AddNew.PaneItem = observer((props: any) => {
); );
}); });
AddNew.Displayed = observer((props: any) => {
const { displayName, children } = props;
const displayed = useDisplayedMapContext();
const { schema } = useDesignable();
useEffect(() => {
if (displayName) {
displayed.set(displayName, schema);
}
}, [displayName, schema]);
return children;
});
export default AddNew; export default AddNew;

View File

@ -56,8 +56,4 @@ export const linkTo: ISchema = {
'x-component': 'Checkbox', 'x-component': 'Checkbox',
}, },
}, },
operations: [
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
],
}; };

View File

@ -1,11 +1,13 @@
import { import {
FormProvider,
ISchema,
observer, observer,
RecursionField, RecursionField,
Schema, Schema,
useField, useField,
useForm, useForm,
} from '@formily/react'; } from '@formily/react';
import { Pagination, Table as AntdTable } from 'antd'; import { Pagination, Popover, Table as AntdTable } from 'antd';
import { findIndex, forIn, range, set } from 'lodash'; import { findIndex, forIn, range, set } from 'lodash';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { useContext } from 'react'; import { useContext } from 'react';
@ -22,8 +24,12 @@ import { CSS } from '@dnd-kit/utilities';
import { Select, Dropdown, Menu, Switch, Button, Space } from 'antd'; import { Select, Dropdown, Menu, Switch, Button, Space } from 'antd';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import './style.less'; import './style.less';
import { getSchemaPath } from '../../components/schema-renderer'; import {
import { options } from '../database-field/interfaces'; getSchemaPath,
SchemaField,
SchemaRenderer,
} from '../../components/schema-renderer';
import { interfaces, options } from '../database-field/interfaces';
import { DraggableBlockContext } from '../../components/drag-and-drop'; import { DraggableBlockContext } from '../../components/drag-and-drop';
import AddNew from '../add-new'; import AddNew from '../add-new';
import { isGridRowOrCol } from '../grid'; import { isGridRowOrCol } from '../grid';
@ -35,6 +41,9 @@ import {
useDisplayedMapContext, useDisplayedMapContext,
} from '../../constate'; } from '../../constate';
import { useResource as useGeneralResource } from '../../hooks/useResource'; import { useResource as useGeneralResource } from '../../hooks/useResource';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { useMemo } from 'react';
import { createForm } from '@formily/core';
const SyntheticListenerMapContext = createContext(null); const SyntheticListenerMapContext = createContext(null);
@ -247,6 +256,7 @@ const useTableColumns = () => {
schema, schema,
props: { rowKey }, props: { rowKey },
} = useTable(); } = useTable();
const { designable } = useDesignable();
const { getField } = useCollectionContext(); const { getField } = useCollectionContext();
@ -284,40 +294,18 @@ const useTableColumns = () => {
}, },
}; };
}) })
.concat([ .concat(
{ designable
title: <AddColumn />, ? [
dataIndex: 'addnew', {
}, title: <AddColumn />,
]); dataIndex: 'addnew',
},
]
: [],
);
}; };
function SwitchField(props) {
const { field, onChange } = props;
const [checked, setChecked] = useState(props.checked);
useEffect(() => {
setChecked(props.checked);
}, [props.checked]);
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
onClick={async () => {
setChecked((checked) => {
onChange(!checked);
return !checked;
});
}}
>
<span>{field.uiSchema.title}</span>
<Switch checked={checked} size={'small'} />
</div>
);
}
function AddColumn() { function AddColumn() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { appendChild, remove } = useDesignable(); const { appendChild, remove } = useDesignable();
@ -333,31 +321,29 @@ function AddColumn() {
<Menu> <Menu>
<Menu.ItemGroup className={'display-fields'} title={'字段展示'}> <Menu.ItemGroup className={'display-fields'} title={'字段展示'}>
{fields.map((field) => ( {fields.map((field) => (
<Menu.Item style={{ minWidth: 150 }}> <SwitchMenuItem
<SwitchField title={field?.uiSchema?.title}
checked={displayed.has(field.name)} checked={displayed.has(field.name)}
field={field} onChange={async (checked) => {
onChange={async (checked) => { if (checked) {
if (checked) { const data = appendChild({
const data = appendChild({ type: 'void',
type: 'void', 'x-component': 'Table.Column',
'x-component': 'Table.Column', 'x-component-props': {
'x-component-props': { fieldName: field.name,
fieldName: field.name, },
}, 'x-designable-bar': 'Table.Column.DesignableBar',
'x-designable-bar': 'Table.Column.DesignableBar', });
}); await createSchema(data);
await createSchema(data); } else {
} else { const s: any = displayed.get(field.name);
const s: any = displayed.get(field.name); const p = getSchemaPath(s);
const p = getSchemaPath(s); const removed = remove(p);
const removed = remove(p); await removeSchema(removed);
await removeSchema(removed); displayed.remove(field.name);
displayed.remove(field.name); }
} }}
}} />
/>
</Menu.Item>
))} ))}
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.Divider /> <Menu.Divider />
@ -382,7 +368,9 @@ function AddColumn() {
</Menu> </Menu>
} }
> >
<Button size={'small'} type={'dashed'} icon={<PlusOutlined />}></Button> <Button type={'dashed'} icon={<PlusOutlined />}>
</Button>
</Dropdown> </Dropdown>
); );
} }
@ -582,7 +570,7 @@ const useTotal = () => {
}; };
Table.Pagination = observer(() => { Table.Pagination = observer(() => {
const { field, service, pagination, setPagination, props } = useTable(); const { service, pagination, setPagination, props } = useTable();
if (!pagination || Object.keys(pagination).length === 0) { if (!pagination || Object.keys(pagination).length === 0) {
return null; return null;
} }
@ -590,7 +578,7 @@ Table.Pagination = observer(() => {
const total = useTotal(); const total = useTotal();
const { page = 1 } = pagination; const { page = 1 } = pagination;
return ( return (
<div> <div style={{ marginTop: 16 }}>
<Pagination <Pagination
{...pagination} {...pagination}
showSizeChanger showSizeChanger
@ -617,8 +605,90 @@ Table.Pagination = observer(() => {
); );
}); });
function generateActionSchema(type) {
const actions: { [key: string]: ISchema } = {
filter: {
key: uid(),
name: uid(),
type: 'void',
title: '筛选',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'filter',
},
'x-component': 'Table.Filter',
'x-designable-bar': 'Table.Filter.DesignableBar',
'x-component-props': {
fieldNames: [],
},
},
export: {},
create: {
key: uid(),
type: 'void',
name: uid(),
title: '新增',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
'x-designable-bar': 'Table.Action.DesignableBar',
properties: {
modal: {
type: 'void',
title: '新增数据',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
'x-component-props': {
useOkAction: '{{ Table.useTableCreateAction }}',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},
},
destroy: {
key: uid(),
type: 'void',
name: uid(),
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
update: {},
};
return actions[type];
}
function AddActionButton() { function AddActionButton() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { appendChild, remove } = useDesignable();
const { schema, designable } = useDesignable();
if (!designable) {
return null;
}
return ( return (
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}
@ -626,25 +696,31 @@ function AddActionButton() {
onVisibleChange={setVisible} onVisibleChange={setVisible}
overlay={ overlay={
<Menu> <Menu>
<Menu.ItemGroup title={'要展示的操作'}> <Menu.ItemGroup title={'操作展示'}>
{[ {[
{ title: '筛选', key: 'filter' }, { title: '筛选', name: 'filter' },
{ title: '导出', key: 'export' }, // { title: '导出', name: 'export' },
{ title: '新增', key: 'create' }, { title: '新增', name: 'create' },
{ title: '删除', key: 'destroy' }, { title: '删除', name: 'destroy' },
].map((item) => ( ].map((item) => (
<Menu.Item style={{ minWidth: 150 }}> <SwitchMenuItem
<div key={item.name}
style={{ checked={displayed.has(item.name)}
display: 'flex', title={item.title}
alignItems: 'center', onChange={async (checked) => {
justifyContent: 'space-between', if (!checked) {
}} const s = displayed.get(item.name) as Schema;
> const path = getSchemaPath(s);
<span>{item.title}</span> displayed.remove(item.name);
<Switch size={'small'} defaultChecked /> const removed = remove(path);
</div> await removeSchema(removed);
</Menu.Item> } else {
const s = generateActionSchema(item.name);
const data = appendChild(s);
await createSchema(data);
}
}}
/>
))} ))}
</Menu.ItemGroup> </Menu.ItemGroup>
<Menu.Divider /> <Menu.Divider />
@ -656,28 +732,233 @@ function AddActionButton() {
</Menu> </Menu>
} }
> >
<Button type={'dashed'} icon={<PlusOutlined />}> <Button style={{ marginLeft: 8 }} type={'dashed'} icon={<PlusOutlined />}>
</Button> </Button>
</Dropdown> </Dropdown>
); );
} }
function Actions(props: any) {
const { align = 'left' } = props;
const { schema, designable } = useDesignable();
return (
<Space>
{schema.mapProperties((s) => {
const currentAlign = s['x-align'] || 'left';
if (currentAlign !== align) {
return null;
}
return <RecursionField name={s.name} schema={s} />;
})}
</Space>
);
}
Table.ActionBar = observer((props: any) => { Table.ActionBar = observer((props: any) => {
const { align = 'top' } = props; const { align = 'top' } = props;
const { schema, designable } = useDesignable(); const { schema, designable } = useDesignable();
const designableBar = schema['x-designable-bar']; const designableBar = schema['x-designable-bar'];
console.log('Table.ActionBar', schema); console.log('Table.ActionBar', { schema });
return ( return (
<div className={cls('nb-action-bar', `align-${align}`)}> <DisplayedMapProvider>
<Space> <div className={cls('nb-action-bar', `align-${align}`)}>
{props.children} <div>
{designable && designableBar && <AddActionButton />} <Actions align={'left'} />
</Space> </div>
</div> <div style={{ marginLeft: 'auto' }}>
<Actions align={'right'} />
</div>
<AddActionButton />
</div>
</DisplayedMapProvider>
); );
}); });
Table.Filter = observer((props: any) => {
const { fieldNames = [] } = props;
const { schema, DesignableBar } = useDesignable();
const form = useMemo(() => createForm(), []);
const { fields = [] } = useCollectionContext();
const fields2properties = (fields: any[]) => {
const properties = {};
fields.forEach((field, index) => {
if (fieldNames?.length && !fieldNames.includes(field.name)) {
return;
}
const fieldOption = interfaces.get(field.interface);
if (!fieldOption.operations) {
return;
}
properties[`column${index}`] = {
type: 'void',
title: field?.uiSchema?.title,
'x-component': 'Filter.Column',
'x-component-props': {
operations: fieldOption.operations,
},
properties: {
[field.name]: {
...field.uiSchema,
title: null,
},
},
};
});
return properties;
};
return (
<Popover
trigger={['click']}
placement={'bottomLeft'}
content={
<div>
<FormProvider form={form}>
<SchemaField
schema={{
type: 'object',
properties: {
filter: {
type: 'object',
'x-component': 'Filter',
properties: fields2properties(fields),
},
},
}}
/>
</FormProvider>
</div>
}
>
<Button>
{schema.title}
<DesignableBar />
</Button>
</Popover>
);
});
Table.Filter.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { fields } = useCollectionContext();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.ItemGroup title={'筛选字段'}>
{fields
.filter((field) => {
const option = interfaces.get(field.interface);
return option.operations?.length;
})
.map((field) => (
<SwitchMenuItem
title={field?.uiSchema?.title}
checked={true}
onChange={async (checked) => {}}
/>
))}
</Menu.ItemGroup>
<Menu.Divider />
<Menu.Item
onClick={(e) => {
schema.title = uid();
refresh();
}}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={async () => {
const displayName =
schema?.['x-decorator-props']?.['displayName'];
const data = remove();
await removeSchema(data);
if (displayName) {
displayed.remove(displayName);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};
Table.OperationDesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const isPopup = Object.keys(schema.properties || {}).length > 0;
const inActionBar = schema.parent['x-component'] === 'Table.ActionBar';
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.ItemGroup title={'操作展示'}>
{[
{ title: '查看', name: 'view' },
{ title: '编辑', name: 'update' },
{ title: '删除', name: 'destroy' },
].map((item) => (
<SwitchMenuItem
key={item.name}
title={item.title}
onChange={async (checked) => {}}
/>
))}
</Menu.ItemGroup>
<Menu.Divider />
<Menu.SubMenu title={'自定义'}>
<Menu.Item style={{ minWidth: 120 }}></Menu.Item>
<Menu.Item></Menu.Item>
<Menu.Item></Menu.Item>
</Menu.SubMenu>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};
Table.Action = () => null; Table.Action = () => null;
Table.Action.DesignableBar = () => { Table.Action.DesignableBar = () => {
@ -685,7 +966,7 @@ Table.Action.DesignableBar = () => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const isPopup = Object.keys(schema.properties || {}).length > 0; const isPopup = Object.keys(schema.properties || {}).length > 0;
const inActionBar = schema.parent['x-component'] === 'Table.ActionBar'; const inActionBar = schema.parent['x-component'] === 'Table.ActionBar';
const displayed = useDisplayedMapContext();
return ( return (
<div className={cls('designable-bar', { active: visible })}> <div className={cls('designable-bar', { active: visible })}>
<span <span
@ -725,17 +1006,27 @@ Table.Action.DesignableBar = () => {
</Menu.Item> </Menu.Item>
)} )}
{inActionBar ? ( {!inActionBar && (
<Menu.Item></Menu.Item>
) : (
<Menu.Item> <Menu.Item>
&nbsp;&nbsp; &nbsp;&nbsp;
<Switch size={'small'} defaultChecked /> <Switch size={'small'} defaultChecked />
</Menu.Item> </Menu.Item>
)} )}
<Menu.Divider /> <Menu.Divider />
<Menu.Item></Menu.Item> <Menu.Item
onClick={async () => {
const displayName =
schema?.['x-decorator-props']?.['displayName'];
const data = remove();
await removeSchema(data);
if (displayName) {
displayed.remove(displayName);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu> </Menu>
} }
> >

View File

@ -67,6 +67,7 @@
&.align-bottom { &.align-bottom {
margin-top: 8px; margin-top: 8px;
} }
display: flex;
} }
th.nb-table-operation, th.nb-table-operation,