feat: action bar for the descriptions block

This commit is contained in:
chenos 2021-08-05 18:34:58 +08:00
parent 41cbb26b2e
commit 8f746ae319
8 changed files with 429 additions and 27 deletions

View File

@ -1,3 +1,3 @@
import { createContext } from "react";
export const VisibleContext = createContext<any>(null);
export const VisibleContext = createContext<any>([]);

View File

@ -0,0 +1,256 @@
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { ISchema, observer, RecursionField, Schema } from '@formily/react';
import React, { useState } from 'react';
import { createSchema, removeSchema, updateSchema } from '..';
import {
findPropertyByPath,
getSchemaPath,
useDesignable,
} from '../../components/schema-renderer';
import { DisplayedMapProvider, useDisplayedMapContext } from '../../constate';
import cls from 'classnames';
import { Button, Dropdown, Menu, Space } from 'antd';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { uid } from '@formily/shared';
import { PlusOutlined } from '@ant-design/icons';
import { Droppable, SortableItem } from '../../components/Sortable';
export const ActionBar = observer((props: any) => {
const { align = 'top' } = props;
// const { schema, designable } = useDesignable();
const { root, schema, insertAfter, remove, appendChild } = useDesignable();
const moveToAfter = (path1, path2, extra = {}) => {
if (!path1 || !path2) {
return;
}
if (path1.join('.') === path2.join('.')) {
return;
}
const data = findPropertyByPath(root, path1);
if (!data) {
return;
}
remove(path1);
return insertAfter(
{
...data.toJSON(),
...extra,
},
path2,
);
};
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<DndContext
onDragStart={(event) => {
setDragOverlayContent(event.active.data?.current?.title || '');
// const previewRef = event.active.data?.current?.previewRef;
// if (previewRef) {
// setDragOverlayContent(previewRef?.current?.innerHTML);
// } else {
// setDragOverlayContent('');
// }
}}
onDragEnd={async (event) => {
const path1 = event.active?.data?.current?.path;
const path2 = event.over?.data?.current?.path;
const align = event.over?.data?.current?.align;
const draggable = event.over?.data?.current?.draggable;
if (!path1 || !path2) {
return;
}
if (path1.join('.') === path2.join('.')) {
return;
}
if (!draggable) {
console.log('alignalignalignalign', align);
const p = findPropertyByPath(root, path1);
if (!p) {
return;
}
remove(path1);
const data = appendChild(
{
...p.toJSON(),
'x-align': align,
},
path2,
);
await updateSchema(data);
} else {
const data = moveToAfter(path1, path2, {
'x-align': align,
});
await updateSchema(data);
}
}}
>
<DragOverlay style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}>
{dragOverlayContent}
{/* <div style={{ transform: 'translateX(-100%)' }} dangerouslySetInnerHTML={{__html: dragOverlayContent}}></div> */}
</DragOverlay>
<DisplayedMapProvider>
<div className={cls('nb-action-bar', `align-${align}`)}>
<div style={{ width: '50%' }}>
<Actions align={'left'} />
</div>
<div style={{ marginLeft: 'auto', width: '50%', textAlign: 'right' }}>
<Actions align={'right'} />
</div>
<AddActionButton />
</div>
</DisplayedMapProvider>
</DndContext>
);
});
function generateActionSchema(type) {
const actions: { [key: string]: ISchema } = {
update: {
type: 'void',
title: '编辑',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'update',
},
'x-component': 'Action',
'x-component-props': {
type: 'primary',
},
'x-action-type': 'update',
'x-designable-bar': 'Action.DesignableBar',
properties: {
modal: {
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: {
type: 'void',
title: '删除',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'destroy',
},
'x-action-type': 'destroy',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
};
return actions[type];
}
function AddActionButton() {
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { appendChild, remove } = useDesignable();
const { schema, designable } = useDesignable();
if (!designable) {
return null;
}
return (
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={setVisible}
overlay={
<Menu>
<Menu.ItemGroup title={'操作展示'}>
{[
{ title: '编辑', name: 'update' },
{ title: '删除', name: 'destroy' },
].map((item) => (
<SwitchMenuItem
key={item.name}
checked={displayed.has(item.name)}
title={item.title}
onChange={async (checked) => {
if (!checked) {
const s = displayed.get(item.name) as Schema;
const path = getSchemaPath(s);
displayed.remove(item.name);
const removed = remove(path);
await removeSchema(removed);
} else {
const s = generateActionSchema(item.name);
const data = appendChild(s);
await createSchema(data);
}
}}
/>
))}
</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>
}
>
<Button style={{ marginLeft: 8 }} type={'dashed'} icon={<PlusOutlined />}>
</Button>
</Dropdown>
);
}
function Actions(props: any) {
const { align = 'left' } = props;
const { schema, designable } = useDesignable();
return (
<Droppable
id={`${schema.name}-${align}`}
className={`action-bar-align-${align}`}
data={{ align, path: getSchemaPath(schema) }}
>
<Space>
{schema.mapProperties((s) => {
const currentAlign = s['x-align'] || 'left';
if (currentAlign !== align) {
return null;
}
return (
<SortableItem
id={s.name}
data={{
align,
draggable: true,
title: s.title,
path: getSchemaPath(s),
}}
>
<RecursionField name={s.name} schema={s} />
</SortableItem>
);
})}
</Space>
</Droppable>
);
}

View File

@ -420,3 +420,28 @@ export default () => {
);
};
```
### Action.Bar
```tsx
import React from 'react';
import { SchemaRenderer } from '@nocobase/client';
const schema = {
type: 'void',
name: 'actionbar1',
'x-component': 'Action.Bar',
'x-designable-bar': 'Action.Bar.DesignableBar',
'x-component-props': {
},
};
export default () => {
return (
<SchemaRenderer
schema={schema}
/>
);
};
```

View File

@ -11,23 +11,41 @@ import {
useField,
SchemaExpressionScopeContext,
} from '@formily/react';
import { Button, Dropdown, Menu, Popover, Space, Drawer, Modal } from 'antd';
import { Link, useHistory, LinkProps } from 'react-router-dom';
import { useDesignable, useDefaultAction, updateSchema } from '..';
import {
Button,
Dropdown,
Menu,
Popover,
Space,
Drawer,
Modal,
Select,
} from 'antd';
import { Link, useHistory, LinkProps, Switch } from 'react-router-dom';
import {
useDesignable,
useDefaultAction,
updateSchema,
removeSchema,
} 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 { FormDialog, FormLayout } from '@formily/antd';
import IconPicker from '../../components/icon-picker';
import {
findPropertyByPath,
getSchemaPath,
SchemaField,
useSchemaComponent,
} from '../../components/schema-renderer';
import { VisibleContext } from '../../context';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { createPortal } from 'react-dom';
import { ActionBar } from './ActionBar';
import { DragHandle } from '../../components/Sortable';
import { useDisplayedMapContext } from '../../constate';
export const ButtonComponentContext = createContext(null);
@ -90,6 +108,8 @@ Action.Modal = observer((props: any) => {
const { run: runOk } = useOkAction();
const { run: runCancel } = useCancelAction();
const isFormDecorator = schema['x-decorator'] === 'Form';
const field = useField();
console.log('Action.Modal.field', field);
return (
<Modal
title={schema.title}
@ -304,10 +324,11 @@ Action.Popover = observer((props) => {
});
Action.DesignableBar = (props: any) => {
const field = useField();
// const schema = useFieldSchema();
const { schema, insertAfter, refresh } = useDesignable(props.path);
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const isPopup = Object.keys(schema.properties || {}).length > 0;
const displayed = useDisplayedMapContext();
const field = useField();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
@ -316,6 +337,7 @@ Action.DesignableBar = (props: any) => {
}}
className={cls('designable-bar-actions', { active: visible })}
>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
@ -325,25 +347,85 @@ Action.DesignableBar = (props: any) => {
overlay={
<Menu>
<Menu.Item
onClick={(e) => {
console.log('按钮文案被修改了', { schema });
schema.title = '按钮文案被修改了';
// field.setTitle('按钮文案被修改了');
// setVisible(false);
onClick={async (e) => {
const values = await FormDialog('修改名称和图标', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '按钮名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '按钮图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
schema['title'] = values.title;
schema['x-component-props']['icon'] = values.icon;
field.componentProps.icon = values.icon;
field.title = values.title;
updateSchema(schema);
refresh();
}}
>
</Menu.Item>
{isPopup && (
<Menu.Item>
{' '}
<Select
bordered={false}
size={'small'}
defaultValue={'Action.Modal'}
onChange={(value) => {
const s = Object.values(schema.properties).shift();
s['x-component'] = value;
refresh();
updateSchema(s);
}}
>
<Select.Option value={'Action.Modal'}></Select.Option>
<Select.Option value={'Action.Drawer'}></Select.Option>
<Select.Option value={'Action.Window'}>
</Select.Option>
</Select>{' '}
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item
onClick={() => {
insertAfter({
name: uid(),
'x-component': 'Input',
});
onClick={async () => {
const displayName =
schema?.['x-decorator-props']?.['displayName'];
const data = remove();
await removeSchema(data);
if (displayName) {
displayed.remove(displayName);
}
setVisible(false);
}}
>
insertAfter
</Menu.Item>
</Menu>
}
@ -354,3 +436,5 @@ Action.DesignableBar = (props: any) => {
</div>
);
};
Action.Bar = ActionBar;

View File

@ -225,7 +225,6 @@ function generateCardItemSchema(component) {
properties: {
[uid()]: {
type: 'void',
name: 'action1',
'x-component': 'Action',
'x-component-props': {
icon: 'EllipsisOutlined',
@ -844,6 +843,12 @@ AddNew.PaneItem = observer((props: any) => {
},
'x-designable-bar': 'Form.DesignableBar',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Action.Bar',
'x-designable-bar': 'Action.Bar.DesignableBar',
'x-component-props': {},
},
[uid()]: {
type: 'void',
'x-component': 'Grid',

View File

@ -37,6 +37,18 @@ import {
useDisplayedMapContext,
} from '../../constate';
import { useResource as useGeneralResource } from '../../hooks/useResource';
import { Resource } from '../../resource';
import { BaseResult } from '@ahooksjs/use-request/lib/types';
export interface FormReadPrettyContextProps {
resource?: Resource;
service?: BaseResult<any, any>;
}
export const FormContext = createContext<FormReadPrettyContextProps>({});
export const FormReadPrettyContext = createContext<FormReadPrettyContextProps>(
{},
);
const FormMain = (props: any) => {
const {
@ -50,9 +62,11 @@ const FormMain = (props: any) => {
readPretty: schema['x-read-pretty'],
});
}, []);
const { resource, run } = useResource({
const { resource, run, service } = useResource({
onSuccess: (initialValues: any) => {
console.log('onSuccess', { initialValues });
form.setInitialValues(initialValues);
form.setValues(initialValues);
},
});
const path = useSchemaPath();
@ -66,7 +80,7 @@ const FormMain = (props: any) => {
console.log(displayed.map, 'displayed.map', collection?.name);
}
}, [displayed.map]);
return (
const content = (
<FormProvider form={form}>
{schema['x-decorator'] === 'Form' ? (
<SchemaField
@ -119,6 +133,14 @@ const FormMain = (props: any) => {
)}
</FormProvider>
);
return schema['x-read-pretty'] ? (
<FormReadPrettyContext.Provider value={{ resource, service }}>
{content}
</FormReadPrettyContext.Provider>
) : (
<>{content}</>
);
};
export const Form: any = observer((props: any) => {

View File

@ -70,6 +70,8 @@ import { isValid } from '@formily/shared';
import { FormButtonGroup, FormDialog, FormLayout, Submit } from '@formily/antd';
import flatten from 'flat';
import IconPicker from '../../components/icon-picker';
import { FormReadPrettyContext } from '../form';
import { VisibleContext } from '../../context';
export interface ITableContext {
props: any;
@ -153,6 +155,7 @@ const useTableUpdateAction = () => {
} = useTable();
const ctx = useContext(TableRowContext);
const form = useForm();
const { service: formService } = useContext(FormReadPrettyContext);
return {
async run() {
@ -160,7 +163,11 @@ const useTableUpdateAction = () => {
await resource.save(form.values, {
resourceKey: ctx.record[rowKey],
});
return service.refresh();
if (formService) {
await formService.refresh();
}
await service.refresh();
return;
}
field.value[ctx.index] = form.values;
// refresh();
@ -179,6 +186,7 @@ const useTableDestroyAction = () => {
props: { refreshRequestOnChange, rowKey },
} = useTable();
const ctx = useContext(TableRowContext);
const [, setVisible] = useContext(VisibleContext);
return {
async run() {
if (refreshRequestOnChange) {
@ -190,6 +198,7 @@ const useTableDestroyAction = () => {
[`${rowKey}.in`]: rowKeys,
});
setSelectedRowKeys([]);
setVisible && setVisible(false);
return service.refresh();
}
if (ctx) {
@ -873,7 +882,7 @@ function generateMenuActionSchema(type) {
properties: {
[uid()]: {
type: 'void',
title: '查看',
title: '查看数据',
'x-component': 'Action.Modal',
'x-component-props': {
bodyStyle: {
@ -2011,7 +2020,7 @@ Table.useResource = ({ onSuccess }) => {
resourceName: collection.name,
resourceKey: ctx.record[props.rowKey],
});
const { data, loading, run } = useRequest(
const service = useRequest(
(params?: any) => {
console.log('Table.useResource', params);
return resource.get(params);
@ -2022,7 +2031,7 @@ Table.useResource = ({ onSuccess }) => {
manual: true,
},
);
return { initialValues: data, loading, run, resource };
return { resource, service, initialValues: service.data, ...service };
};
Table.useTableFilterAction = useTableFilterAction;

View File

@ -69,6 +69,7 @@
}
}
.ant-drawer-body,
.ant-card-body,
.ant-modal-body {
.nb-tabs {