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;
+}