feat: action bar for the descriptions block
This commit is contained in:
parent
41cbb26b2e
commit
8f746ae319
@ -1,3 +1,3 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export const VisibleContext = createContext<any>(null);
|
||||
export const VisibleContext = createContext<any>([]);
|
||||
|
256
packages/client/src/schemas/action/ActionBar.tsx
Normal file
256
packages/client/src/schemas/action/ActionBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -69,6 +69,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-drawer-body,
|
||||
.ant-card-body,
|
||||
.ant-modal-body {
|
||||
.nb-tabs {
|
||||
|
Loading…
Reference in New Issue
Block a user