diff --git a/packages/client/src/schema-initializer/SchemaInitializer.tsx b/packages/client/src/schema-initializer/SchemaInitializer.tsx index a1cddfc98..183f44159 100644 --- a/packages/client/src/schema-initializer/SchemaInitializer.tsx +++ b/packages/client/src/schema-initializer/SchemaInitializer.tsx @@ -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 ; } - 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) => { > { insertAdjacent(insertPosition, wrap(schema)); }} @@ -68,6 +75,7 @@ SchemaInitializer.Button = observer((props: any) => { > { 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 ; + } if (item.type === 'itemGroup') { return ( - + // @ts-ignore + {renderMenuItem(item.children)} ); @@ -116,13 +128,19 @@ SchemaInitializer.Item = (props) => { if (item.type === 'subMenu') { return ( // @ts-ignore - + {renderMenuItem(item.children)} ); } return ( - + { + onClick({ ...info, item }); + }} + > {item.title} ); @@ -136,8 +154,19 @@ SchemaInitializer.Item = (props) => { ); } return ( - + { + onClick({ ...opts, item: info }); + }} + {...others} + > {children} ); }; + +SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => { + return component; +}; diff --git a/packages/client/src/schema-initializer/demos/demo1.tsx b/packages/client/src/schema-initializer/demos/demo1.tsx index 86a95390e..9deeb7ca5 100644 --- a/packages/client/src/schema-initializer/demos/demo1.tsx +++ b/packages/client/src/schema-initializer/demos/demo1.tsx @@ -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; + const items: any = [ + { + type: 'itemGroup', + title: 'select a data source', + children: [ + { + type: 'item', + title: 'Users', + }, + { + type: 'item', + title: 'Posts', + }, + ], + }, + ]; return ( } - onClick={(info) => { - console.log({ info }); + items={items} + onClick={({ item }) => { + // 如果有 items 时,onClick 里会返回点击的 item + // TODO: 实际情况,这里还需要补充更完整的初始化逻辑 insert({ type: 'void', - title: info.key, + title: item.title, 'x-component': 'Hello', }); }} - items={[ - { - type: 'itemGroup', - title: 'select a data source', - children: [ - { - key: 'users', - title: 'Users', - }, - { - key: 'posts', - title: 'Posts', - }, - ], - }, - ]} - > - Table - + /> ); -}; +}); -const FormBlockInitializerItem = (props) => { +const FormBlockInitializerItem = SchemaInitializer.itemWrap((props) => { const { insert } = props; return ( } - onClick={(info) => { + onClick={() => { insert({ type: 'void', - title: 'form', + title: 'Form', 'x-component': 'Hello', }); }} - > - Form - + /> ); -}; +}); export default function App() { return ( diff --git a/packages/client/src/schema-initializer/demos/demo2.tsx b/packages/client/src/schema-initializer/demos/demo2.tsx index bf14a92e6..d61812da8 100644 --- a/packages/client/src/schema-initializer/demos/demo2.tsx +++ b/packages/client/src/schema-initializer/demos/demo2.tsx @@ -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 ( { + onClick={() => { if (exists) { return remove(); } insert({ type: 'void', 'x-component': 'Action', - ...schema, + ...item.schema, }); }} >
- {title} + {item.title}
); -}; +}); 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', }, diff --git a/packages/client/src/schema-initializer/demos/demo3.tsx b/packages/client/src/schema-initializer/demos/demo3.tsx index 4e14ee1ee..9f6e82a89 100644 --- a/packages/client/src/schema-initializer/demos/demo3.tsx +++ b/packages/client/src/schema-initializer/demos/demo3.tsx @@ -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 ( { @@ -99,22 +102,22 @@ const InitializeFormItem = (props) => { return remove(); } insert({ - ...schema, + ...item.schema, }); }} >
- {title} + {item.title}
); -}; +}); -const InitializeTextFormItem = (props) => { - const { title, insert } = props; +const InitializeTextFormItem = SchemaInitializer.itemWrap((props) => { + const { insert } = props; return ( { + onClick={() => { insert({ type: 'void', 'x-component': 'Markdown.Void', @@ -122,11 +125,9 @@ const InitializeTextFormItem = (props) => { // 'x-editable': false, }); }} - > - {title} - + /> ); -}; +}); const Page = (props) => { return ( diff --git a/packages/client/src/schema-initializer/demos/demo4.tsx b/packages/client/src/schema-initializer/demos/demo4.tsx index 878b96b1e..31bc5be62 100644 --- a/packages/client/src/schema-initializer/demos/demo4.tsx +++ b/packages/client/src/schema-initializer/demos/demo4.tsx @@ -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 ( { @@ -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, }, }, }); }} >
- {title} + {item.title}
); -}; +}); const schema = { type: 'object', diff --git a/packages/client/src/schema-initializer/index.md b/packages/client/src/schema-initializer/index.md index aca365ba2..a4cdb8367 100644 --- a/packages/client/src/schema-initializer/index.md +++ b/packages/client/src/schema-initializer/index.md @@ -7,7 +7,25 @@ group: # SchemaInitializer -用于配置各种 schema 的初始化 +用于各种 schema 的初始化。新增的 schema 可以插入到某个已有 schema 节点的任意位置,包括: + +```ts +{ + properties: { + // beforeBegin 在当前节点的前面插入 + node1: { + properties: { + // afterBegin 在当前节点的第一个子节点前面插入 + // ... + // beforeEnd 在当前节点的最后一个子节点后面 + }, + }, + // afterEnd 在当前节点的后面 + }, +} +``` + +SchemaInitializer 的核心包括 `` 和 `` 两个组件。`` 是用于创建 Schema 的 Dropdown 按钮,按钮有上下文,表示新增的 schema 要插入的位置,可以通过 `insertPosition` 属性来指定插入的具体位置。下拉菜单里的为 ``,用于自定义各种 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()` 可获得更好的类型提示。 + +`` 的下拉菜单项,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 ( } - 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 ); -}; +}); ``` ## Examples diff --git a/packages/client/src/schema-initializer/index.ts b/packages/client/src/schema-initializer/index.ts index 84a16f4f3..d7c9df468 100644 --- a/packages/client/src/schema-initializer/index.ts +++ b/packages/client/src/schema-initializer/index.ts @@ -1 +1,3 @@ export * from './SchemaInitializer'; +export * from './types'; + diff --git a/packages/client/src/schema-initializer/types.ts b/packages/client/src/schema-initializer/types.ts new file mode 100644 index 000000000..e73b61bf0 --- /dev/null +++ b/packages/client/src/schema-initializer/types.ts @@ -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 { + items?: SchemaInitializerItemOptions[]; + onClick?: MenuClickEventHandler; +} + +type MenuClickEventHandler = (info: MenuInfo) => void; + +interface MenuInfo { + // key: string; + // keyPath: string[]; + item: ItemOptions; + domEvent: React.MouseEvent | React.KeyboardEvent; +}