feat(client): improve schema Initializer

This commit is contained in:
chenos 2022-02-09 00:13:42 +08:00
parent 4baded4702
commit 6990347013
8 changed files with 219 additions and 105 deletions

View File

@ -1,15 +1,21 @@
import { observer, Schema } from '@formily/react';
import { ISchema, observer } from '@formily/react';
import { useDesignable } from '@nocobase/client';
import { Button, Dropdown, Menu } from 'antd';
import React, { createContext, useContext, useState } from 'react';
import {
SchemaInitializerButtonProps,
SchemaInitializerItemComponent,
SchemaInitializerItemOptions,
SchemaInitializerItemProps
} from './types';
const defaultWrap = (s: Schema) => s;
const defaultWrap = (s: ISchema) => s;
export const SchemaInitializerItemContext = createContext(null);
export const SchemaInitializer = () => null;
SchemaInitializer.Button = observer((props: any) => {
SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
const { wrap = defaultWrap, items = [], insertPosition, dropdown, ...others } = props;
const { insertAdjacent, findComponent } = useDesignable();
const [visible, setVisible] = useState(false);
@ -20,7 +26,7 @@ SchemaInitializer.Button = observer((props: any) => {
if (item.type === 'divider') {
return <Menu.Divider key={`item-${indexA}`} />;
}
if (item.component) {
if (item.type === 'item' && item.component) {
const Component = findComponent(item.component);
return (
Component && (
@ -37,6 +43,7 @@ SchemaInitializer.Button = observer((props: any) => {
>
<Component
{...item}
item={item}
insert={(schema) => {
insertAdjacent(insertPosition, wrap(schema));
}}
@ -68,6 +75,7 @@ SchemaInitializer.Button = observer((props: any) => {
>
<Component
{...child}
item={child}
insert={(schema) => {
insertAdjacent(insertPosition, wrap(schema));
}}
@ -97,18 +105,22 @@ SchemaInitializer.Button = observer((props: any) => {
);
});
SchemaInitializer.Item = (props) => {
const { items = [], children, icon, onClick, ...others } = props;
SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
const { index, info } = useContext(SchemaInitializerItemContext);
const { items = [], children = info?.title, icon, onClick, ...others } = props;
if (items?.length > 0) {
const renderMenuItem = (items) => {
const renderMenuItem = (items: SchemaInitializerItemOptions[]) => {
if (!items?.length) {
return null;
}
return items.map((item, indexA) => {
if (item.type === 'divider') {
return <Menu.Divider key={`divider-${indexA}`} />;
}
if (item.type === 'itemGroup') {
return (
<Menu.ItemGroup eventKey={indexA} key={indexA} title={item.title}>
// @ts-ignore
<Menu.ItemGroup eventKey={`item-group-${indexA}`} key={`item-group-${indexA}`} title={item.title}>
{renderMenuItem(item.children)}
</Menu.ItemGroup>
);
@ -116,13 +128,19 @@ SchemaInitializer.Item = (props) => {
if (item.type === 'subMenu') {
return (
// @ts-ignore
<Menu.SubMenu eventKey={indexA} key={indexA} title={item.title}>
<Menu.SubMenu eventKey={`sub-menu-${indexA}`} key={`sub-menu-${indexA}`} title={item.title}>
{renderMenuItem(item.children)}
</Menu.SubMenu>
);
}
return (
<Menu.Item eventKey={item.key} key={item.key} onClick={onClick}>
<Menu.Item
eventKey={item.key}
key={item.key}
onClick={(info) => {
onClick({ ...info, item });
}}
>
{item.title}
</Menu.Item>
);
@ -136,8 +154,19 @@ SchemaInitializer.Item = (props) => {
);
}
return (
<Menu.Item eventKey={info.key || index} icon={icon} onClick={onClick} {...others}>
<Menu.Item
eventKey={index}
icon={icon}
onClick={(opts) => {
onClick({ ...opts, item: info });
}}
{...others}
>
{children}
</Menu.Item>
);
};
SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => {
return component;
};

View File

@ -20,13 +20,16 @@ const AddBlockButton = observer((props: any) => {
insertPosition={'beforeBegin'}
items={[
{
type: 'itemGroup',
title: 'Data blocks',
children: [
{
type: 'item',
title: 'Table',
component: TableBlockInitializerItem,
},
{
type: 'item',
title: 'Form',
component: FormBlockInitializerItem,
},
@ -39,58 +42,56 @@ const AddBlockButton = observer((props: any) => {
);
});
const TableBlockInitializerItem = (props) => {
const TableBlockInitializerItem = SchemaInitializer.itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
onClick={(info) => {
console.log({ info });
insert({
type: 'void',
title: info.key,
'x-component': 'Hello',
});
}}
items={[
const items: any = [
{
type: 'itemGroup',
title: 'select a data source',
children: [
{
key: 'users',
type: 'item',
title: 'Users',
},
{
key: 'posts',
type: 'item',
title: 'Posts',
},
],
},
]}
>
Table
</SchemaInitializer.Item>
];
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
items={items}
onClick={({ item }) => {
// 如果有 items 时onClick 里会返回点击的 item
// TODO: 实际情况,这里还需要补充更完整的初始化逻辑
insert({
type: 'void',
title: item.title,
'x-component': 'Hello',
});
}}
/>
);
};
});
const FormBlockInitializerItem = (props) => {
const FormBlockInitializerItem = SchemaInitializer.itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<FormOutlined />}
onClick={(info) => {
onClick={() => {
insert({
type: 'void',
title: 'form',
title: 'Form',
'x-component': 'Hello',
});
}}
>
Form
</SchemaInitializer.Item>
/>
);
};
});
export default function App() {
return (

View File

@ -17,11 +17,12 @@ const AddActionButton = observer((props: any) => {
style={{ marginLeft: 8 }}
items={[
{
type: 'itemGroup',
title: 'Enable actions',
children: [
{
type: 'item',
title: 'Create',
key: 'create',
component: InitializeAction,
schema: {
title: 'Create',
@ -30,13 +31,13 @@ const AddActionButton = observer((props: any) => {
},
},
{
type: 'item',
title: 'Update',
key: 'update',
component: InitializeAction,
schema: {
title: 'Update',
'x-action': 'posts:update',
},
component: InitializeAction,
},
],
},
@ -65,28 +66,28 @@ const useCurrentActionSchema = (action: string) => {
};
};
const InitializeAction = (props) => {
const { title, schema, insert } = props;
const { exists, remove } = useCurrentActionSchema(schema['x-action']);
const InitializeAction = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentActionSchema(item.schema['x-action']);
return (
<SchemaInitializer.Item
onClick={(info) => {
onClick={() => {
if (exists) {
return remove();
}
insert({
type: 'void',
'x-component': 'Action',
...schema,
...item.schema,
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{title} <Switch size={'small'} checked={exists} />
{item.title} <Switch size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
};
});
export default function App() {
return (
@ -96,11 +97,15 @@ export default function App() {
type: 'void',
name: 'page',
'x-component': 'ActionBar',
// 指定初始化的按钮组件,
// Table、Form、Details、Calendar、Kanban 等等不同区块
// 可以根据情况组装自己的 initializer
'x-action-initializer': 'AddActionButton',
properties: {
action1: {
type: 'void',
title: 'Update',
// 使用 x-action 来标记 action schema
'x-action': 'posts:update',
'x-component': 'Action',
},

View File

@ -6,6 +6,7 @@ import {
SchemaComponent,
SchemaComponentProvider,
SchemaInitializer,
SchemaInitializerItemOptions,
useDesignable
} from '@nocobase/client';
import { Input, Switch } from 'antd';
@ -14,7 +15,7 @@ import React from 'react';
const useFormItemInitializerFields = () => {
return [
{
key: 'name',
type: 'item',
title: 'Name',
component: InitializeFormItem,
schema: {
@ -26,7 +27,7 @@ const useFormItemInitializerFields = () => {
},
},
{
key: 'title',
type: 'item',
title: 'Title',
component: InitializeFormItem,
schema: {
@ -37,7 +38,7 @@ const useFormItemInitializerFields = () => {
'x-collection-field': 'posts.title',
},
},
];
] as SchemaInitializerItemOptions[];
};
const AddFormItemButton = observer((props: any) => {
@ -47,6 +48,7 @@ const AddFormItemButton = observer((props: any) => {
insertPosition={'beforeEnd'}
items={[
{
type: 'itemGroup',
title: 'Display fields',
children: useFormItemInitializerFields(),
},
@ -54,6 +56,7 @@ const AddFormItemButton = observer((props: any) => {
type: 'divider',
},
{
type: 'item',
title: 'Add text',
component: InitializeTextFormItem,
},
@ -89,9 +92,9 @@ const useCurrentFieldSchema = (path: string) => {
};
};
const InitializeFormItem = (props) => {
const { title, schema, insert } = props;
const { exists, remove } = useCurrentFieldSchema(schema['x-collection-field']);
const InitializeFormItem = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentFieldSchema(item.schema['x-collection-field']);
return (
<SchemaInitializer.Item
onClick={() => {
@ -99,22 +102,22 @@ const InitializeFormItem = (props) => {
return remove();
}
insert({
...schema,
...item.schema,
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{title} <Switch size={'small'} checked={exists} />
{item.title} <Switch size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
};
});
const InitializeTextFormItem = (props) => {
const { title, insert } = props;
const InitializeTextFormItem = SchemaInitializer.itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
onClick={(info) => {
onClick={() => {
insert({
type: 'void',
'x-component': 'Markdown.Void',
@ -122,11 +125,9 @@ const InitializeTextFormItem = (props) => {
// 'x-editable': false,
});
}}
>
{title}
</SchemaInitializer.Item>
/>
);
};
});
const Page = (props) => {
return (

View File

@ -5,15 +5,16 @@ import {
SchemaComponent,
SchemaComponentProvider,
SchemaInitializer,
SchemaInitializerItemOptions,
useDesignable
} from '@nocobase/client';
import { Switch } from 'antd';
import React from 'react';
const useTableColumnInitializerFields = () => {
const fields = [
const fields: SchemaInitializerItemOptions[] = [
{
key: 'name',
type: 'item',
title: 'Name',
schema: {
type: 'string',
@ -24,7 +25,7 @@ const useTableColumnInitializerFields = () => {
component: ColumnInitializerItem,
},
{
key: 'title',
type: 'item',
title: 'Title',
schema: {
type: 'string',
@ -45,6 +46,7 @@ export const AddTableColumn = observer((props: any) => {
insertPosition={'beforeEnd'}
items={[
{
type: 'itemGroup',
title: 'Display fields',
children: useTableColumnInitializerFields(),
},
@ -80,9 +82,9 @@ const useCurrentColumnSchema = (path: string) => {
};
};
export const ColumnInitializerItem = (props) => {
const { title, schema, insert } = props;
const { exists, remove } = useCurrentColumnSchema(schema['x-collection-field']);
export const ColumnInitializerItem = SchemaInitializer.itemWrap((props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentColumnSchema(item.schema['x-collection-field']);
return (
<SchemaInitializer.Item
onClick={() => {
@ -91,23 +93,23 @@ export const ColumnInitializerItem = (props) => {
}
insert({
type: 'void',
title: schema.name,
title: item.schema.name,
'x-component': 'ArrayTable.Column',
properties: {
[schema.name]: {
[item.schema.name]: {
'x-read-pretty': true,
...schema,
...item.schema,
},
},
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{title} <Switch size={'small'} checked={exists} />
{item.title} <Switch size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
};
});
const schema = {
type: 'object',

View File

@ -7,7 +7,25 @@ group:
# SchemaInitializer
用于配置各种 schema 的初始化
用于各种 schema 的初始化。新增的 schema 可以插入到某个已有 schema 节点的任意位置,包括:
```ts
{
properties: {
// beforeBegin 在当前节点的前面插入
node1: {
properties: {
// afterBegin 在当前节点的第一个子节点前面插入
// ...
// beforeEnd 在当前节点的最后一个子节点后面
},
},
// afterEnd 在当前节点的后面
},
}
```
SchemaInitializer 的核心包括 `<SchemaInitializer.Button/>``<SchemaInitializer.Item/>` 两个组件。`<SchemaInitializer.Button/>` 是用于创建 Schema 的 Dropdown 按钮,按钮有上下文,表示新增的 schema 要插入的位置,可以通过 `insertPosition` 属性来指定插入的具体位置。下拉菜单里的为 `<SchemaInitializer.Item/>`,用于自定义各种 schema 的初始化逻辑schema 可以是区块、字段、操作等。
## SchemaInitializer.Button
@ -16,13 +34,16 @@ group:
```tsx | pure
const items = [
{
type: 'itemGroup',
title: 'Data blocks',
children: [
{
type: 'item',
title: 'Table',
component: 'TableBlockInitializerItem',
},
{
type: 'item',
title: 'Form',
component: 'FormBlockInitializerItem',
},
@ -51,8 +72,9 @@ const useFormItemInitializerFields = () => {
const { fields } = useCollection();
return fields.map(field => {
return {
key: field.name,
component: 'FormItemInitializerItem'
type: 'item',
component: 'FormItemInitializerItem',
schema: {}, // TODO, 例如从 field.uiSchema 里获取
}
});
}
@ -77,43 +99,38 @@ export const AddFieldButton = () => {
## SchemaInitializer.Item
扩展项
用于自定义各种 schema 的初始化逻辑,配合 `SchemaInitializer.itemWrap()` 可获得更好的类型提示。
`<SchemaInitializer.Button/>` 的下拉菜单项items 属性里 type 为 item 的 component
```ts
{
type: 'item',
title: 'Table',
component: 'TableBlockInitializerItem',
}
```
例子如下:
```tsx | pure
const TableBlockInitializerItem = (props) => {
const TableBlockInitializerItem = SchemaInitializer.itemWrap((props) => {
const { insert } = props;
return (
<SchemaInitializer.Item
icon={<TableOutlined />}
onClick={(info) => {
console.log({ info });
onClick={() => {
// 插入的 schema在这里补充更完整的逻辑
insert({
type: 'void',
title: info.key,
'x-component': 'Hello',
'x-component': 'Table',
});
}}
items={[
{
type: 'itemGroup',
title: 'select a data source',
children: [
{
key: 'users',
title: 'Users',
},
{
key: 'posts',
title: 'Posts',
},
],
},
]}
>
Table
</SchemaInitializer.Item>
);
};
});
```
## Examples

View File

@ -1 +1,3 @@
export * from './SchemaInitializer';
export * from './types';

View File

@ -0,0 +1,57 @@
import { ISchema } from '@formily/react';
import { ButtonProps, DropDownProps, MenuItemProps } from 'antd';
export interface SchemaInitializerButtonProps extends ButtonProps {
wrap?: (s: ISchema) => ISchema;
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
items?: SchemaInitializerItemOptions[];
dropdown?: DropDownProps;
}
export type SchemaInitializerItemOptions = ItemGroupOptions | SubMenuOptions | ItemOptions | DividerOptions;
interface ItemCommonOptions {
title?: any;
}
interface ItemGroupOptions extends ItemCommonOptions {
type: 'itemGroup';
children?: SchemaInitializerItemOptions[];
}
interface SubMenuOptions extends ItemCommonOptions {
type: 'subMenu';
children?: SchemaInitializerItemOptions[];
}
interface ItemOptions extends ItemCommonOptions {
type: 'item';
component?: any;
schema?: ISchema;
[key: string]: any;
}
interface DividerOptions {
type: 'divider';
}
export type SchemaInitializerItemComponent = (props?: SchemaInitializerItemComponentProps) => any;
interface SchemaInitializerItemComponentProps {
insert?: (s: ISchema) => void;
item?: ItemOptions;
}
export interface SchemaInitializerItemProps extends Omit<MenuItemProps, 'onClick'> {
items?: SchemaInitializerItemOptions[];
onClick?: MenuClickEventHandler;
}
type MenuClickEventHandler = (info: MenuInfo) => void;
interface MenuInfo {
// key: string;
// keyPath: string[];
item: ItemOptions;
domEvent: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>;
}