diff --git a/.dumirc.ts b/.dumirc.ts index dfcc3746bf..45a2ef213b 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -30,6 +30,12 @@ export default defineConfig({ alias: { ...umiConfig.alias, }, + ssr: { + + }, + exportStatic: { + ignorePreRenderError: true + }, cacheDirectoryPath: `node_modules/.docs-${lang}-cache`, outputPath: `./docs/dist/${lang}`, resolve: { diff --git a/.github/workflows/deploy-client-docs.yml b/.github/workflows/deploy-client-docs.yml index a6faffe693..a3a8bea87f 100644 --- a/.github/workflows/deploy-client-docs.yml +++ b/.github/workflows/deploy-client-docs.yml @@ -23,8 +23,10 @@ jobs: with: node-version: "18" - run: yarn install - - name: Build - run: yarn doc build core/client + - name: Build zh-CN + run: yarn doc build core/client --lang=zh-CN + - name: Build en-US + run: yarn doc build core/client --lang=en-US - name: Set tags id: set-tags run: | diff --git a/.github/workflows/nocobase-test-e2e.yml b/.github/workflows/nocobase-test-e2e.yml index 9e4aba89fe..9399498c4b 100644 --- a/.github/workflows/nocobase-test-e2e.yml +++ b/.github/workflows/nocobase-test-e2e.yml @@ -76,4 +76,4 @@ jobs: DB_UNDERSCORED: ${{ matrix.underscored }} DB_SCHEMA: ${{ matrix.schema }} COLLECTION_MANAGER_SCHEMA: ${{ matrix.collection_schema }} - timeout-minutes: 40 + timeout-minutes: 60 diff --git a/docs/en-US/api/acl/acl.md b/docs/en-US/api/acl/acl.md index 0e8e28bc76..568b1766d2 100644 --- a/docs/en-US/api/acl/acl.md +++ b/docs/en-US/api/acl/acl.md @@ -206,14 +206,14 @@ type ConditionFunc = (ctx: any) => Promise | boolean; **Detailed Information** * resourceName - Name of the resource -* actionNames - Name of the resource action +* actionNames - Name of the resource action * condition? - Configuration of the validity condition * Pass in a `string` to use a condition that is already defined; Use the `acl.allowManager.registerCondition` method to register a condition. ```typescript acl.allowManager.registerAllowCondition('superUser', async () => { return ctx.state.user?.id === 1; }); - + // Open permissions of the users:list with validity condition superUser acl.allow('users', 'list', 'superUser'); ``` diff --git a/docs/en-US/api/client/application.md b/docs/en-US/api/client/application.md index 38387db903..340ed42f99 100644 --- a/docs/en-US/api/client/application.md +++ b/docs/en-US/api/client/application.md @@ -35,7 +35,6 @@ Add Providers, build-in Providers are: - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider -- SchemaInitializerProvider - BlockSchemaComponentProvider - AntdSchemaComponentProvider - ACLProvider diff --git a/docs/en-US/api/client/schema-designer/schema-initializer.md b/docs/en-US/api/client/schema-designer/schema-initializer.md index a8673a125b..7e047db625 100644 --- a/docs/en-US/api/client/schema-designer/schema-initializer.md +++ b/docs/en-US/api/client/schema-designer/schema-initializer.md @@ -17,11 +17,3 @@ Used for the initialization of various schemas. Newly added schema can be insert }, } ``` - -The core of SchemaInitializer includes `` and `` the two components. `` is used to create the dropdown menu button of schema, and the options of the dropdown menu is ``. - -### `` - -### `` - -### `` diff --git a/docs/en-US/development/client/index.md b/docs/en-US/development/client/index.md index 6080eff9a3..9cfbc00dd4 100644 --- a/docs/en-US/development/client/index.md +++ b/docs/en-US/development/client/index.md @@ -10,7 +10,6 @@ Most of the extensions for the NocoBase client are provided as Providers. - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider -- SchemaInitializerProvider - BlockSchemaComponentProvider - AntdSchemaComponentProvider - DocumentTitleProvider diff --git a/docs/en-US/development/client/ui-schema-designer/x-initializer.md b/docs/en-US/development/client/ui-schema-designer/x-initializer.md index 6f2c7c61fe..46e6320722 100644 --- a/docs/en-US/development/client/ui-schema-designer/x-initializer.md +++ b/docs/en-US/development/client/ui-schema-designer/x-initializer.md @@ -25,15 +25,11 @@ ```tsx |pure import React, { useContext } from 'react'; -import { SchemaInitializerContext } from '@nocobase/client'; +import { Plugin } from '@nocobase/client'; -export default React.memo((props) => { - const items = useContext(SchemaInitializerContext); - const BlockInitializers = {}; - return ( - - {props.children} - - ); -}); +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add('BlockInitializers', BlockInitializers) + } +} ``` diff --git a/docs/zh-CN/api/client/application.md b/docs/zh-CN/api/client/application.md index 7f8ac6e035..36ee426bba 100644 --- a/docs/zh-CN/api/client/application.md +++ b/docs/zh-CN/api/client/application.md @@ -35,7 +35,6 @@ const app = new Application({ - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider -- SchemaInitializerProvider - BlockSchemaComponentProvider - AntdSchemaComponentProvider - ACLProvider diff --git a/docs/zh-CN/api/client/schema-designer/schema-initializer.md b/docs/zh-CN/api/client/schema-designer/schema-initializer.md index 1502fd496e..32a6a844a2 100644 --- a/docs/zh-CN/api/client/schema-designer/schema-initializer.md +++ b/docs/zh-CN/api/client/schema-designer/schema-initializer.md @@ -17,11 +17,3 @@ }, } ``` - -SchemaInitializer 的核心包括 `` 和 `` 两个组件。`` 用于创建 Schema 的下拉菜单按钮,下拉菜单的菜单项为 ``。 - -### `` - -### `` - -### `` diff --git a/docs/zh-CN/development/client/index.md b/docs/zh-CN/development/client/index.md index c1e257850a..7e20252689 100644 --- a/docs/zh-CN/development/client/index.md +++ b/docs/zh-CN/development/client/index.md @@ -10,7 +10,6 @@ NocoBase 客户端的扩展大多以 Provider 的形式提供。 - SystemSettingsProvider - PluginManagerProvider - SchemaComponentProvider -- SchemaInitializerProvider - BlockSchemaComponentProvider - AntdSchemaComponentProvider - DocumentTitleProvider diff --git a/docs/zh-CN/development/client/ui-schema-designer/x-initializer.md b/docs/zh-CN/development/client/ui-schema-designer/x-initializer.md index 7932e60626..1a16683c6e 100644 --- a/docs/zh-CN/development/client/ui-schema-designer/x-initializer.md +++ b/docs/zh-CN/development/client/ui-schema-designer/x-initializer.md @@ -23,17 +23,13 @@ ## 替换 -```tsx |pure +```tsx | pure import React, { useContext } from 'react'; -import { SchemaInitializerContext } from '@nocobase/client'; +import { Plugin } from '@nocobase/client'; -export default React.memo((props) => { - const items = useContext(SchemaInitializerContext); - const BlockInitializers = {}; - return ( - - {props.children} - - ); -}); +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add('BlockInitializers', BlockInitializers) + } +} ``` diff --git a/docs/zh-CN/welcome/release/v15-changelog.md b/docs/zh-CN/welcome/release/v15-changelog.md new file mode 100644 index 0000000000..99e85254c6 --- /dev/null +++ b/docs/zh-CN/welcome/release/v15-changelog.md @@ -0,0 +1,278 @@ +# v0.15 + +## 不兼容的变化 + +### SchemaInitializer 的注册和实现变更 + +#### 定义方式变更 + +以前 `SchemaInitializer` 支持 2 种定义方式,分别为对象和组件。例如: + +```tsx +const BlockInitializers = { + title: '{{t("Add block")}}', + icon: 'PlusOutlined', + items: [ + // ... + ], + // ... +} +``` + +```tsx +const BlockInitializers = () => { + return +} +``` + +现在仅支持 `new SchemaInitializer()` 的实例。例如: + +```tsx +const blockInitializers = new SchemaInitializer({ + name: 'BlockInitializers', // 名称,和原来保持一致 + title: '{{t("Add block")}}', + icon: 'PlusOutlined', + items: [ + // ... + ], + // ... +}); +``` + +#### 参数变更 + +整体来说,`new SchemaInitializer()` 的参数参考了之前对象定义方式,但又有新的变更。具体如下: + +- 新增 `name` 必填参数,用于 `x-initializer` 的值。 +- 新增 `Component` 参数,用于定制化渲染的按钮。默认为 `SchemaInitializerButton`。 +- 新增 `componentProps`、`style` 用于配置 `Component` 的属性和样式。 +- 新增 `ItemsComponent` 参数,用于定制化渲染的列表。默认为 `SchemaInitializerItems`。 +- 新增 `itemsComponentProps`、`itemsComponentStyle` 用于配置 `ItemsComponent` 的属性和样式。 +- 新增 `popover` 参数,用于配置是否显示 `popover` 效果。 +- 新增 `useInsert` 参数,用于当 `insert` 函数需要使用 hooks 时。 +- 更改 将 `dropdown` 参数改为了 `popoverProps`,使用 `Popover` 代替了 `Dropdown`。 +- items 参数变更 + - 新增 `useChildren` 函数,用于动态控制子项。 + - 更改 `visible` 参数改为了 `useVisible` 函数,用于动态控制是否显示。 + - 更改 将 `component` 参数改为了 `Component`,用于列表项的渲染。 + - 更改 将 `key` 参数改为了 `name`,用于列表项的唯一标识。 + +案例1: + +```diff +- export const BlockInitializers = { ++ export const blockInitializers = new SchemaInitializer({ ++ name: 'BlockInitializers', + 'data-testid': 'add-block-button-in-page', + title: '{{t("Add block")}}', + icon: 'PlusOutlined', + wrap: gridRowColWrap, + items: [ + { +- key: 'dataBlocks', ++ name: 'data-blocks', + type: 'itemGroup', + title: '{{t("Data blocks")}}', + children: [ + { +- key: 'table', ++ name: 'table', +- type: 'item', // 当有 Component 参数时,就不需要此了 + title: '{{t("Table")}}', +- component: TableBlockInitializer, ++ Component: TableBlockInitializer, + }, + { + key: 'form', + type: 'item', + title: '{{t("Form")}}', + component: FormBlockInitializer, + } + ], + }, + ], +}); +``` + +案例2: + +原来是组件定义的方式: + +```tsx +export const BulkEditFormItemInitializers = (props: any) => { + const { t } = useTranslation(); + const { insertPosition, component } = props; + const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' }); + return ( + + ); +}; +``` + +现在需要改为 `new SchemaInitializer()` 的方式: + +```tsx +const bulkEditFormItemInitializers = new SchemaInitializer({ + name: 'BulkEditFormItemInitializers', + 'data-testid': 'configure-fields-button-of-bulk-edit-form-item', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + // 原 insertPosition 和 component 是透传的,这里不用管,也是透传的 + items: [ + { + type: 'itemGroup', + title: t('Display fields'), + name: 'display-fields', // 记得加上 name + useChildren: useCustomBulkEditFormItemInitializerFields, // 使用到了 useChildren + }, + { + type: 'divider', + }, + { + title: t('Add text'), + name: 'add-text', + Component: BlockItemInitializer, // component 替换为 Component + }, + ] +}); +``` + +关于参数的具体说明可以参考 `SchemaInitializer` 的类型定义,以及 [SchemaInitializer 文档](https://client.docs.nocobase.com/client/schema-initializer)。 + +#### 实现原理变更 + +以前是将所有 `items` 转为 `Menu` 组件的 items JSON 对象,最后渲染成 Menu 列表。 + +现在默认情况下仅仅是渲染 `items` 列表项的 `Component` 组件,至于 `Component` 组件内部如何渲染取决于自身,最后也不会拼接成一个 JSON 对象。 + +具体说明参考 `SchemaInitializer` 的 [Nested items 示例](https://client.docs.nocobase.com/client/schema-initializer#nested-items)。 + +#### 列表中的组件获取参数方式变更 + +之前是通过 `props` 获取 `insert` 函数,现在需要通过 `useSchemaInitializer()` 获取。例如: + +```diff +const FormBlockInitializer = (props) => { +- const { insert } = props; ++ const { insert } = useSchemaInitializer(); + // ... +} + +export const blockInitializers = new SchemaInitializer({ + name: 'BlockInitializers', + items: [ + { + name: 'form', + Component: FormBlockInitializer + } + ] +}); +``` + +#### 注册方式变更 + +以前是通过 `SchemaInitializerProvider` 进行注册。例如: + +```tsx + +``` + +现在需要改为插件的方式。例如: + +```tsx +import { Plugin } from '@nocobase/client'; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(blockInitializers); + this.app.addComponents({ ManualActionDesigner }); + } +} +``` + +#### 修改方式变更 + +以前是通过 `SchemaInitializerContext` 获取到全部的 `Initializers` 然后进行增删改。例如下面代码是为了往 `BlockInitializers` 中的 `media` 下添加 `Hello`: + +```tsx +const items = useContext(SchemaInitializerContext); +const mediaItems = items.BlockInitializers.items.find((item) => item.key === 'media'); + +if (process.env.NODE_ENV !== 'production' && !mediaItems) { + throw new Error('media block initializer not found'); +} + +const children = mediaItems.children; +if (!children.find((item) => item.key === 'hello')) { + children.push({ + key: 'hello', + type: 'item', + title: '{{t("Hello block")}}', + component: HelloBlockInitializer, + }); +} +``` + +新的方式则通过插件的方式更简洁的进行修改。例如: + +```tsx +class MyPlugin extends Plugin { + async load() { + // 获取 BlockInitializers + const blockInitializers = this.app.schemaInitializerManager.get('BlockInitializers'); + + // 添加 Hello + blockInitializers.add('media.hello', { + title: '{{t("Hello block")}}', + Component: HelloBlockInitializer, + }) + } +} +``` + +#### 使用方式变更 + +之前使用 `useSchemaInitializer` 的方式进行渲染,现在需要改为 `useSchemaInitializerRender`,并且参数需要增加 `x-initializer-props`。例如: + +```diff +- const { render } = useSchemaInitializer(fieldSchema['x-initializer']); ++ const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); + +render(); +render({ style: { marginLeft: 8 } }) +``` + + +更多说明可以参考 [SchemaInitializer 文档](https://client.docs.nocobase.com/client/schema-initializer)。 diff --git a/package.json b/package.json index 3eaf388e67..7c8d4a02c6 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "axios": "^0.26.1", "commander": "^9.2.0", "dumi": "^2.2.0", - "dumi-theme-nocobase": "^0.2.14", + "dumi-theme-nocobase": "^0.2.18", "eslint-plugin-jest-dom": "^5.0.1", "eslint-plugin-testing-library": "^5.11.0", "execa": "^5.1.1", @@ -103,7 +103,7 @@ "ts-jest": "^29.1.1", "typescript": "5.1.3", "vite": "^4.4.9", - "vitest": "^0.34.3" + "vitest": "^0.34.6" }, "volta": { "node": "18.14.2", diff --git a/packages/core/build/src/build.ts b/packages/core/build/src/build.ts index 142c378200..201ff256fc 100755 --- a/packages/core/build/src/build.ts +++ b/packages/core/build/src/build.ts @@ -24,7 +24,13 @@ export async function build(pkgs: string[]) { const packages = getPackages(pkgs); if (packages.length === 0) { - console.error(chalk.red(`[@nocobase/build]: '${pkgs.join(', ')}' not match any packages.`)); + let msg = ''; + if (pkgs.length) { + msg = `'${pkgs.join(', ')}' did not match any packages` + } else { + msg = 'No package matched' + } + console.warn(chalk.yellow(`[@nocobase/build]: ${msg}`)); return; } diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index 6821e2b941..2f5ce67ee2 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -3,19 +3,29 @@ import { defineConfig } from 'dumi'; import { defineThemeConfig } from 'dumi-theme-nocobase'; const umiConfig = getUmiConfig(); +process.env.DOC_LANG = process.env.DOC_LANG || 'zh-CN'; +const lang = process.env.DOC_LANG; + +console.log('process.env.DOC_LANG', lang); export default defineConfig({ hash: true, alias: { ...umiConfig.alias, }, + // ssr: {}, + // exportStatic: { + // ignorePreRenderError: true + // }, + cacheDirectoryPath: `node_modules/.docs-client-${lang}-cache`, + outputPath: `./dist/${lang}`, resolve: { - atomDirs: [ - { type: 'api', dir: 'src' }, - { type: 'api', dir: 'src/schema-component/antd' }, - { type: 'api', dir: 'src/route-switch/antd' }, - ], + docDirs: [`./docs/${lang}`] }, + locales: [ + { id: 'en-US', name: 'English' }, + { id: 'zh-CN', name: '中文' }, + ], themeConfig: defineThemeConfig({ title: 'NocoBase', logo: 'https://www.nocobase.com/images/logo.png', @@ -25,92 +35,338 @@ export default defineConfig({ nav: [ { title: 'API', - link: '/apis/application', + link: '/core/application/application', }, + // { + // title: 'UI Schema', + // link: '/ui-schema', + // }, ], sidebarEnhance: { - '/apis': [ + '/core': [ { - title: 'Core', + title: 'Application', type: 'group', children: [ { title: 'Application', - children: [ - { - title: 'Application', - link: '/apis/application', - }, - { - title: 'APIClient', - link: '/apis/api-client', - }, - { - title: 'PluginSettingsManager', - link: '#', - }, - ], + link: '/core/application/application', }, { - title: 'UI schema designer', - children: [ - { - title: 'SchemaComponent', - link: '#', - }, - { - title: 'SchemaInitializer', - link: '#', - }, - { - title: 'SchemaSettings', - link: '#', - }, - { - title: 'DNDContext & DragHandler', - link: '#', - }, - ], + title: 'Plugin', + link: '/core/application/plugin', }, { - title: 'Collection Manager', - link: '#', + title: 'PluginManager', + link: '/core/application/plugin-manager', }, { - title: 'BlockProvider', - link: '#', + title: 'RouterManager', + link: '/core/application/router-manager', }, { - title: 'RecordProvider', - link: '#', + title: 'PluginSettingsManager', + link: '/core/application/plugin-settings-manager', }, ], }, { - title: 'React components', + title: 'UI Schema', type: 'group', children: [ { - title: 'Board', - link: '#', + title: 'SchemaComponent', + link: '/core/ui-schema/schema-component', }, { - title: 'Icon', - link: '#', + title: 'Designable', + link: '/core/ui-schema/designable', }, - ], - }, - { - title: 'Schema components', - type: 'group', - children: [ { - title: 'Input', - link: '#', + title: 'SchemaInitializer', + link: '/core/ui-schema/schema-initializer', + }, + { + title: 'SchemaInitializerManager', + link: '/core/ui-schema/schema-initializer-manager', + }, + { + title: 'SchemaSettings', + link: '/core/ui-schema/schema-settings', + }, + { + title: 'SchemaSettingsManager', + link: '/core/ui-schema/schema-settings-manager', + }, + { + title: 'SchemaToolbar', + link: '/core/ui-schema/schema-toolbar', }, ], }, ], + // '/ui-schema': [ + // { + // title: 'Overview', + // link: '/ui-schema', + // }, + // { + // title: 'Globals', + // type: 'group', + // children: [ + // { + // title: 'Menu', + // link: '/ui-schema/globals/menu', + // }, + // { + // title: 'Page', + // link: '/ui-schema/globals/page', + // }, + // { + // title: 'Tabs', + // link: '/ui-schema/globals/tabs', + // }, + // ], + // }, + // { + // title: 'Blocks', + // type: 'group', + // children: [ + // { + // title: 'Overview', + // link: '/ui-schema/blocks', + // }, + // { + // title: 'Data blocks', + // children: [ + // { + // title: 'Overview', + // link: '/ui-schema/blocks/data', + // }, + // { + // title: 'Table', + // link: '/ui-schema/blocks/data/table', + // }, + // { + // title: 'Form', + // link: '/ui-schema/blocks/data/form', + // }, + // { + // title: 'Form(Read pretty)', + // link: '/ui-schema/blocks/data/form-read-pretty', + // }, + // { + // title: 'Details', + // link: '/ui-schema/blocks/data/details', + // }, + // { + // title: 'List', + // link: '/ui-schema/blocks/data/list', + // }, + // { + // title: 'Grid Card', + // link: '/ui-schema/blocks/data/grid-card', + // }, + // { + // title: 'Calendar', + // link: '/ui-schema/blocks/data/calendar', + // }, + // { + // title: 'Kanban', + // link: '/ui-schema/blocks/data/kanban', + // }, + // { + // title: 'Map', + // link: '/ui-schema/blocks/data/map', + // }, + // { + // title: 'Gantt', + // link: '/ui-schema/blocks/data/gantt', + // }, + // { + // title: 'Charts', + // link: '/ui-schema/blocks/data/charts', + // }, + // ], + // }, + // { + // title: 'Filter blocks', + // children: [ + // { + // title: 'Collapse', + // link: '/ui-schema/blocks/filter/collapse', + // }, + // { + // title: 'Form', + // link: '/ui-schema/blocks/filter/form', + // }, + // ], + // }, + // { + // title: 'Other blocks', + // children: [ + // { + // title: 'iframe', + // link: '/ui-schema/blocks/others/iframe', + // }, + // { + // title: 'Markdown', + // link: '/ui-schema/blocks/others/markdown', + // }, + // { + // title: 'Workflow todos', + // link: '/ui-schema/blocks/others/workflow-todo', + // }, + // ], + // }, + // ], + // }, + // { + // title: 'Fields', + // type: 'group', + // children: [ + // { + // title: 'Overview', + // link: '/ui-schema/fields', + // }, + // { + // title: 'FormItem', + // link: '/ui-schema/fields/form-item', + // }, + // { + // title: 'TableColumn', + // link: '/ui-schema/fields/table-column', + // }, + // { + // title: 'Association', + // children: [ + // { + // title: 'Title', + // link: '/ui-schema/fields/association-components/title', + // }, + // { + // title: 'Tag', + // link: '/ui-schema/fields/association-components/tag', + // }, + // { + // title: 'Select', + // link: '/ui-schema/fields/association-components/select', + // }, + // { + // title: 'RecordPicker', + // link: '/ui-schema/fields/association-components/record-picker', + // }, + // { + // title: 'Cascader', + // link: '/ui-schema/fields/association-components/cascader-select', + // }, + // { + // title: 'Sub-form', + // link: '/ui-schema/fields/association-components/sub-form', + // }, + // { + // title: 'Sub-form(Popover)', + // link: '/ui-schema/fields/association-components/sub-form-popover', + // }, + // { + // title: 'Sub-details', + // link: '/ui-schema/fields/association-components/sub-details', + // }, + // { + // title: 'Sub-table', + // link: '/ui-schema/fields/association-components/cascader-select', + // }, + // { + // title: 'File manager', + // link: '/ui-schema/fields/association-components/file-manager', + // }, + // ], + // }, + // ], + // }, + // { + // title: 'Actions', + // type: 'group', + // children: [ + // { + // title: 'Overview', + // link: '/ui-schema/actions', + // }, + // { + // title: 'Add new', + // link: '/ui-schema/actions/add-new', + // }, + // { + // title: 'View', + // link: '/ui-schema/actions/view', + // }, + // { + // title: 'Edit', + // link: '/ui-schema/actions/edit', + // }, + // { + // title: 'Delete', + // link: '/ui-schema/actions/delete', + // }, + // { + // title: 'Submit', + // link: '/ui-schema/actions/submit', + // }, + // { + // title: 'Filter', + // link: '/ui-schema/actions/filter', + // }, + // { + // title: 'Refresh', + // link: '/ui-schema/actions/refresh', + // }, + // { + // title: 'Print', + // link: '/ui-schema/actions/print', + // }, + // { + // title: 'Duplicate', + // link: '/ui-schema/actions/duplicate', + // }, + // { + // title: 'Export', + // link: '/ui-schema/actions/export', + // }, + // { + // title: 'Import', + // link: '/ui-schema/actions/import', + // }, + // { + // title: 'Bulk update', + // link: '/ui-schema/actions/bulk-update', + // }, + // { + // title: 'Bulk edit', + // link: '/ui-schema/actions/bulk-edit', + // }, + // { + // title: 'Add record(任意表)', + // link: '/ui-schema/actions/add-record', + // }, + // { + // title: 'Update record', + // link: '/ui-schema/actions/update-record', + // }, + // { + // title: 'Save record', + // link: '/ui-schema/actions/save-record', + // }, + // { + // title: 'Custom request', + // link: '/ui-schema/actions/custom-request', + // }, + // { + // title: 'Submit to workflow', + // link: '/ui-schema/actions/submit-to-workflow', + // }, + // ], + // }, + // ], }, }), }); diff --git a/packages/core/client/.npmignore b/packages/core/client/.npmignore index f1b6bd97d6..355b528455 100644 --- a/packages/core/client/.npmignore +++ b/packages/core/client/.npmignore @@ -1,3 +1,4 @@ /node_modules /src -/.dumi \ No newline at end of file +/.dumi +/docs diff --git a/packages/core/client/docs/develop.md b/packages/core/client/docs/develop.md deleted file mode 100644 index 72c1138e2c..0000000000 --- a/packages/core/client/docs/develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar: false ---- - - diff --git a/packages/core/client/docs/en-US/core/application/application.md b/packages/core/client/docs/en-US/core/application/application.md new file mode 100644 index 0000000000..77d44d2880 --- /dev/null +++ b/packages/core/client/docs/en-US/core/application/application.md @@ -0,0 +1,369 @@ +# Application + +## new Application(options) + +创建一个 NocoBase 应用。 + +- 类型 + +```tsx | pure +export interface ApplicationOptions { + apiClient?: APIClientOptions; + ws?: WebSocketClientOptions | boolean; + i18n?: i18next; + providers?: (ComponentType | ComponentAndProps)[]; + plugins?: PluginType[]; + components?: Record; + scopes?: Record; + router?: RouterOptions; + schemaSettings?: SchemaSetting[]; + schemaInitializers?: SchemaInitializer[]; + loadRemotePlugins?: boolean; +} +``` + +- 详细信息 + - apiClient:API 请求实例,具体说明请参见:[https://docs.nocobase.com/api/sdk](https://docs.nocobase.com/api/sdk) + - i18n:国际化,具体请参考:[https://www.i18next.com/overview/api#createinstance](https://www.i18next.com/overview/api#createinstance) + - providers:上下文 + - components:全局组件 + - scopes:全局 scopes + - router:配置路由,具体请参考:[RouterManager](/core/application/router-manager) + - pluginSettings: [PluginSettingsManager](/core/application/plugin-settings-manager) + - schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) + - schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) + - loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。 + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import { Application, Plugin } from '@nocobase/client'; + +const ProviderDemo = ({ children }) => { + return
+
hello world
+
{children}
+
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + } +} + +const app = new Application({ + providers: [ProviderDemo], + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +## 实例属性 + +### app.i18n + +```tsx | pure +class Application { + i18n: i18next; +} +``` + +详细介绍,请参考:[i18next](https://www.i18next.com/overview/api#createinstance) + +### app.apiClient + +```tsx | pure +class Application { + apiClient: APIClient; +} +``` + +详细介绍,请参考:[APIClient](https://docs.nocobase.com/api/sdk) + +### app.router + +详细介绍,请参考:[RouterManager](/core/application/router-manager) + +### app.pluginSettingsManager + +详细介绍,请参考:[PluginSettingsManager](/core/application/plugin-settings-manager) + +### app.schemaSettingsManager + +详细介绍,请参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) + +### app.schemaInitializerManager + +详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) + +## 实例方法 + +### app.getRootComponent() + +获取应用的根组件。 + +- 类型 + +```tsx | pure +class Application { + getRootComponent(): React.FC +} +``` + +- 示例 + +```tsx | pure +import { Application } from '@nocobase/client'; + +const app = new Application(); + +const App = app.getRootComponent(); +``` + +### app.mount() + +将应用实例挂载在一个容器元素中。 + +- 类型 + +```tsx | pure +class Application { + mount(containerOrSelector: Element | ShadowRoot | string): ReactDOM.Root +} +``` + +- 示例 + +```tsx | pure +import { Application } from '@nocobase/client'; + +const app = new Application(); + +app.mount('#root'); +``` + +### app.addProvider() + +添加 `Provider` 上下文。 + +- 类型 + +```tsx | pure +class Application { + addProvider(component: ComponentType, props?: T): void; +} +``` + +- 详细信息 + +第一个参数是组件,第二个参数是组件的参数。注意 `Provider` 一定要渲染 `children`。 + +- 示例 + +```tsx | pure +// 场景1:第三方库,或者自己创建的 Context +const MyContext = createContext({}); +app.addProvider(MyContext.provider, { value: { color: 'red' } }); +``` + +```tsx +import { createContext, useContext } from 'react'; +import { Application, Plugin } from '@nocobase/client'; + +const MyContext = createContext(); + +const HomePage = () => { + const { color } = useContext(MyContext) || {}; + return
home page
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.addProvider(MyContext.Provider, { value: { color: 'red' } }); + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +```tsx | pure +// 场景2:自定义的组件,注意 children +const GlobalDemo = ({ name, children }) => { + return
+
hello, { name }
+
{ children }
+
+} +app.addProvider(GlobalDemo, { name: 'nocobase' }); +``` + + +```tsx +import { Application, Plugin } from '@nocobase/client'; + +const GlobalDemo = ({ name, children }) => { + return
+
hello, { name }
+
{ children }
+
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.addProvider(GlobalDemo, { name: 'nocobase' }); + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + + +### app.addProviders() + +添加多个 `Provider` 上下文。 + +- 类型 + +```tsx | pure +class Application { + addProviders(providers: (ComponentType | [ComponentType, any])[]): void; +} +``` + +- 详细信息 + +一次添加多个 `Provider`。 + +- 示例 + +```tsx | pure +app.addProviders([[MyContext.provider, { value: { color: 'red' } }], [GlobalDemo, { name: 'nocobase' }]]) +``` + +### app.addComponents() + +添加全局组件。 + +全局组件可以使用在 [RouterManager](/core/application/router-manager) 和 [UI Schema](/core/ui-schema/schema-component)上。 + +- 类型 + +```tsx | pure +class Application { + addComponents(components: Record): void; +} +``` + +- 示例 + +```tsx | pure +app.addComponents({ Demo, Foo, Bar }) +``` + +### app.addScopes() + +添加全局的 scope。 + +全局组 scope 可以 [UI Schema](/core/ui-schema/schema-component) 上。 + +- 类型 + +```tsx | pure +class Application { + addScopes(scopes: Record): void; +} +``` + +- 示例 + +```tsx | pure +function useSomeThing() {} +const anyVar = ''; + +app.addScopes({ useSomeThing, anyVar }) +``` + +## Hooks + +### useApp() + +获取当前应用的实例。 + +- 类型 + +```tsx | pure +const useApp: () => Application +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const app = useApp(); + return
{ JSON.stringify(app.router.getRouters()) }
+} +``` + +```tsx +import { Application, Plugin, useApp } from '@nocobase/client'; + +const HomePage = () => { + const app = useApp(); + return
{ JSON.stringify(app.router.getRoutes()) }
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` diff --git a/packages/core/client/docs/en-US/core/application/plugin-manager.md b/packages/core/client/docs/en-US/core/application/plugin-manager.md new file mode 100644 index 0000000000..342420f32a --- /dev/null +++ b/packages/core/client/docs/en-US/core/application/plugin-manager.md @@ -0,0 +1,122 @@ +# PluginManager + +用于管理插件。 + +```tsx | pure +class PluginManager { + add(plugin: typeof Plugin, opts?: PluginOptions): Promise + + get(PluginClass: T): InstanceType; + get(name: string): T; +} +``` + +## 实例方法 + +### pluginManager.add() + +将插件添加到应用中。 + +- 类型 + +```tsx | pure +class PluginManager { + add(plugin: typeof Plugin, opts?: PluginOptions): Promise +} +``` + +- 详细信息 + +第一个参数是插件类,第二个则是实例化时传递的参数。前面已经讲过会在添加插件后,立即调用 `afterAdd` 钩子函数,所以返回的是 `Promise`。 + +对于远程组件而言,会自动传递一个 `name` 参数。 + +- 示例 + +```tsx | pure +class MyPluginA extends Plugin { + async load() { + console.log('options', this.options) + console.log('app', this.app); + console.log('router', this.app.router, this.router); + } +} + +class MyPluginB extends Plugin { + // 需要在 afterAdd 执行添加的方法 + async afterAdd() { + // 通过 `app.pluginManager.add()` 添加插件时,第一个参数是插件类,第二个参数是实例化时传递的参数 + this.app.pluginManager.add(MyPluginA, { name: 'MyPluginA', hello: 'world' }) + } +} + +const app = new Application({ + plugins: [MyPluginB], +}); +``` + +### pluginManager.get() + +获取插件实例。 + +- 类型 + +```tsx | pure +class PluginManager { + get(PluginClass: T): InstanceType; + get(name: string): T; +} +``` + +- 详细信息 + +可以通过 Class 获取插件示例,如果在插件注册的时候有 name,也可以通过字符串的 name 获取。 + +如果是远程插件,会自动传入 name,值为 package 的 name。 + +- 示例 + +```tsx | pure +import MyPluginA from 'xxx'; + +class MyPluginB extends Plugin { + async load() { + // 方式1:通过 Class 获取 + const myPluginA = this.app.pluginManager.get(MyPluginA); + + // 方式2:通过 name 获取(添加的时候要传递 name 参数) + const myPluginA = this.app.pluginManager.get('MyPluginA'); + } +} +``` + +## Hooks + +获取插件实例,等同于 `pluginManager.get()`。 + +### usePlugin() + +```tsx | pure +function usePlugin(plugin: T): InstanceType; +function usePlugin(name: string): T; +``` + +- 详细信息 + +可以通过 Class 获取插件示例,如果在插件注册的时候有 name,也可以通过字符串的 name 获取。 + +- 示例 + +```tsx | pure +import { usePlugin } from '@nocobase/client'; + +const Demo = () => { + // 通过 Class 获取 + const myPlugin = usePlugin(MyPlugin); + + // 通过 name 获取(添加的时候要传递 name 参数) + const myPlugin = usePlugin('MyPlugin'); + + return
+} +``` diff --git a/packages/core/client/docs/en-US/core/application/plugin-settings-manager.md b/packages/core/client/docs/en-US/core/application/plugin-settings-manager.md new file mode 100644 index 0000000000..34e6f8c67d --- /dev/null +++ b/packages/core/client/docs/en-US/core/application/plugin-settings-manager.md @@ -0,0 +1,269 @@ +# PluginSettingsManager + +![](../static/plugin-settings.jpg) + +用于管理插件配置页面,其底层对应着 [RouterManager](/core/application/router-manager)。 + +```tsx | pure +interface PluginSettingOptionsType { + title: string; + /** + * @default `Outlet` + */ + Component?: ComponentType | string; + icon?: string; + /** + * sort, the smaller the number, the higher the priority + * @default 0 + */ + sort?: number; + aclSnippet?: string; +} + +interface PluginSettingsPageType { + label?: string; + title: string; + key: string; + icon: any; + path: string; + sort?: number; + name?: string; + isAllow?: boolean; + topLevelName?: string; + aclSnippet: string; + children?: PluginSettingsPageType[]; +} + +class PluginSettingsManager { + add(name: string, options: PluginSettingOptionsType): void + get(name: string, filterAuth?: boolean): PluginSettingsPageType; + getList(filterAuth?: boolean): PluginSettingsPageType[] + has(name: string): boolean; + remove(name: string): void; + getRouteName(name: string): string + getRoutePath(name: string): string; + hasAuth(name: string): boolean; +} +``` + +## 实例方法 + +### pluginSettingsManager.add() + +添加插件配置页。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + add(name: string, options: PluginSettingOptionsType): void +} +``` + +- 详细解释 + +第一个参数 `name`,是路由唯一标识,用于后续的删改查,并且 `name` 支持 `.` 用于分割层级,不过需要注意当使用 `.` 分层的时候,父级要使用 [Outlet](https://reactrouter.com/en/main/components/outlet),让子元素能正常渲染。 + +第二个参数中 `Component` 支持组件形式和字符串形式,如果是字符串组件,要先通过 [app.addComponents](/core/application/application#appaddcomponents) 进行注册,具体参考 [RouterManager](/core/application/router-manager)。 + +- 示例 + +单层级配置。 + +```tsx | pure +const HelloSettingPage = () => { + return
hello setting page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.add('hello', { + title: 'Hello', // menu title and page title + icon: 'ApiOutlined', // menu icon + Component: HelloSettingPage + }) + } +} +``` + +多层级配置。 + +```tsx | pure +// 多层级配置页 + +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.add('hello', { + title: 'HelloWorld', + icon: '', + // Component: Outlet, 默认为 react-router-dom 的 Outlet 组件,可自定义 + }) + + this.app.pluginSettingsManager.add('hello.demo1', { + title: 'Demo1 Page', + Component: () =>
Demo1 Page Content
+ }) + + this.app.pluginSettingsManager.add('hello.demo2', { + title: 'Demo2 Page', + Component: () =>
Demo2 Page Content
+ }) + } +} +``` + +### pluginSettingsManager.get() + +获取配置信息。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + get(name: string, filterAuth?: boolean): PluginSettingsPageType; +} +``` + +- 详细解释 + +第一个是在添加时的 name 参数,第二个参数是是否在获取的时候进行权限过滤。 + +- 示例 + +在组件中获取。 + +```tsx | pure +const Demo = () => { + const app = useApp(); + const helloSettingPage = this.app.pluginSettingsManager.get('hello'); +} +``` + +在插件中获取。 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const helloSettingPage = this.app.pluginSettingsManager.get('hello') + const helloSettingPage = this.app.pluginSettingsManager.get('hello', false); + + const mobileAppConfigPage = this.app.pluginSettingsManager.get('mobile.app') + } +} +``` + +### pluginSettingsManager.getList() + +获取插件配置页列表。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getList(filterAuth?: boolean): PluginSettingsPageType[] +} +``` + +- 详细解释 + +`filterAuth` 默认值为 `true`,即进行权限过滤。 + +- 示例 + +```tsx | pure +const Demo = () => { + const app = useApp(); + const settings = app.pluginSettingsManager.getList(); + const settings = app.pluginSettingsManager.getList(false); +} +``` + +### pluginSettingsManager.has() + +判断是否存在,内部已进行权限过滤。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + has(name: string): boolean; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.has('hello'); + } +} +``` + +### pluginSettingsManager.remove() + +移除配置。 + +```tsx | pure +class PluginSettingsManager { + remove(name: string): void; +} +``` + +### pluginSettingsManager.getRouteName() + +获取对应路由的名称。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getRouteName(name: string): string +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const helloRouteName = this.pluginSettingsManager.getRouteName('hello'); // admin.settings.hello + } +} +``` + +### pluginSettingsManager.getRoutePath() + +获取插件配置对应的页面路径。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getRoutePath(name: string): string; +} +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const navigate = useNavigate(); + const app = useApp(); + const helloSettingPath = app.pluginSettingsManager.getRoutePath('hello'); + + return
navigate(helloSettingPath)}> + go to hello setting page +
+} +``` + +### pluginSettingsManager.hasAuth() + +单独判断是否权限。 + +```tsx | pure +class PluginSettingsManager { + hasAuth(name: string): boolean; +} +``` diff --git a/packages/core/client/docs/en-US/core/application/plugin.md b/packages/core/client/docs/en-US/core/application/plugin.md new file mode 100644 index 0000000000..839bfb964d --- /dev/null +++ b/packages/core/client/docs/en-US/core/application/plugin.md @@ -0,0 +1,85 @@ +# Plugin + +Plugin 基类。 + +- 类型 + +```tsx | pure +class Plugin { + constructor( + protected options: T, + protected app: Application, + ) { + this.options = options; + this.app = app; + } + + get pluginManager() { + return this.app.pluginManager; + } + + get router() { + return this.app.router; + } + + get pluginSettingsManager() { + return this.app.pluginSettingsManager; + } + + get schemaInitializerManager() { + return this.app.schemaInitializerManager; + } + + get schemaSettingsManager() { + return this.app.schemaSettingsManager; + } + + async afterAdd() {} + + async beforeLoad() {} + + async load() {} +} +``` + +- 详细信息 + + - 构造函数 + - `options`: 插件的添加有两种方式,一种方式从插件列表中远程加载出来,另一种方式是通过 [PluginManager](/core/application/plugin-manager) 添加 + - 远程加载:`options` 会被自动注入 `{ name: 'npm package.name' }` + - PluginManager `options` 由用户自己传递 + - `app`:此参数是自动注入的,是应用实例 + - 快捷访问:基类提供了对 `app` 部分方法和属性的快捷访问 + - `pluginManager` + - `router` + - `pluginSettingsManager` + - `schemaSettingsManager` + - `schemaInitializerManager` + - 声明周期 + - `afterAdd`:插件被添加后立即执行 + - `beforeLoad`:执行渲染时执行,在 `afterAdd` 之后,`load` 之前 + - `load`:最后执行 +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + + async afterAdd() { + console.log('afterAdd') + } + + async beforeLoad() { + console.log('beforeLoad') + } + + async load() { + console.log('load') + + // 可以访问应用实例 + console.log(this.app) + + // 访问应用实例内容 + console.log(this.app.router, this.router); + } +} +``` diff --git a/packages/core/client/docs/en-US/core/application/router-manager.md b/packages/core/client/docs/en-US/core/application/router-manager.md new file mode 100644 index 0000000000..ef707219c8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/application/router-manager.md @@ -0,0 +1,386 @@ +# RouterManager + +用于管理路由。 + +```tsx | pure +import { ComponentType } from 'react'; +import { RouteObject } from 'react-router-dom'; + +interface RouteType extends Omit { + Component?: ComponentType | string; +} + +class RouterManager { + add(name: string, route: RouteType): void; + getRoutes(): Record; + getRoutesTree(): RouteObject[]; + get(name: string): RouteType; + has(name: string): boolean; + remove(name: string): void; + setType(type: 'browser' | 'memory' | 'hash'): void; + setBasename(basename: string): void; +} +``` + +## 实例方法 + +### router.add() + +添加一条路由。 + +- 类型 + +```tsx | pure +class RouterManager { + add(name: string, route: RouteType): void +} +``` + +- 详情 + +第一个参数 `name`,是路由唯一标识,用于后续的删改查,并且 `name` 支持 `.` 用于分割层级,不过需要注意当使用 `.` 分层的时候,父级要使用 [Outlet](https://reactrouter.com/en/main/components/outlet),让子元素能正常渲染。 + +第二个参数 `RouteType` 的 `Component` 支持组件形式和字符串形式,如果是字符串组件,要先通过 [app.addComponents](/core/application/application#appaddcomponents) 进行注册。 + +- 示例 + +单层级路由。 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + this.app.router.add('login', { + path: '/login', + element:
login page
+ }) + } +} +``` + +```tsx +import { useNavigate } from 'react-router-dom'; +import { Plugin, Application } from '@nocobase/client'; + +const HomePage = () => { + const navigate = useNavigate(); + return
+
home page
+ +
+} + +const LoginPage = () => { + const navigate = useNavigate(); + return
+
login page
+ +
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + this.app.router.add('login', { + path: '/login', + Component: LoginPage + }) + } +} + + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +多层级路由。 + +```tsx | pure +import { Plugin } from '@nocobase/client'; +import { Outlet } from 'react-router-dom'; + +const AdminLayout = () =>{ + return
+
This is admin layout
+ +
+} + +const AdminSettings = () => { + return
This is admin settings page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('admin', { + path: '/admin', + Component: AdminLayout + }) + this.app.router.add('admin.settings', { + path: '/admin/settings', + Component: AdminSettings , + }) + } +} +``` + + +```tsx +import { useNavigate, Outlet } from 'react-router-dom'; +import { Plugin, Application } from '@nocobase/client'; + +const AdminLayout = () =>{ + return
+
This is admin layout
+ +
+} + +const AdminSettings = () => { + return
This is admin settings page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('admin', { + path: '/admin', + Component: AdminLayout + }) + this.app.router.add('admin.settings', { + path: '/admin/settings', + Component: AdminSettings , + }) + } +} + + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/admin/settings'], + } +}); + +export default app.getRootComponent(); +``` + +`Component` 参数为字符串。 + +```tsx | pure +const LoginPage = () => { + return
login page
+} + +class MyPlugin extends Plugin { + async load() { + // 通过 app.addComponents 进行注册 + this.app.addComponents({ LoginPage }) + + this.app.router.add('login', { + path: '/login', + Component: 'LoginPage', // 这里可以使用字符串了 + }) + } +} +``` + +### router.getRoutes() + +获取路由列表。 + +- 类型 + +```tsx | pure +class RouterManager { + getRoutes(): Record +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + console.log(this.app.router.getRoutes()); + } +} +``` + +![](../static/CxjJbp0pcogYn6xviVyc6QT8nUg.png) + +### router.getRoutesTree() + +获取用于 [useRoutes()](https://reactrouter.com/hooks/use-routes) 的数据。 + +- 类型 + +```tsx | pure +class RouterManager { + getRoutesTree(): RouteObject[] +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const routes = this.app.router.getRoutesTree(); + } +} +``` + +### router.get() + +获取单个路由配置。 + +- 类型 + +```tsx | pure +class RouterManager { + get(name: string): RouteType +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const adminRoute = this.app.router.get('admin') + const adminSettings = this.app.router.get('admin.settings') + } +} +``` + +### router.has() + +判断是否添加过路由。 + +- 类型 + +```tsx | pure +class RouterManager { + has(name: string): boolean; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasAdminRoute = this.app.router.has('admin') + const hasAdminSettings = this.app.router.has('admin.settings') + } +} +``` + +### router.remove() + +移除路由配置。 + +- 类型 + +```tsx | pure +class RouterManager { + remove(name: string): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.remove('admin') + this.app.router.remove('admin.settings') + } +} +``` + +### router.setType() + +设置路由类型,默认为 `browser`。 + + +- 类型 + +```tsx | pure +class RouterManager { + setType(type: 'browser' | 'memory' | 'hash'): void; +} +``` + +- 详细解释 + - browser: [BrowserRouter](https://reactrouter.com/en/main/router-components/browser-router) + - memory: [MemoryRouter](https://reactrouter.com/en/main/router-components/hash-router) + - hash: [HashRouter](https://reactrouter.com/en/main/router-components/memory-router) + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.setType('hash') + } +} +``` + +### router.setBasename() + +设置 [basename](https://reactrouter.com/en/main/router-components/browser-router#basename)。 + +- 类型 + +```tsx | pure +class RouterManager { + setBasename(basename: string): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.setBasename('/') + } +} +``` + +## Hooks + +### useRouter() + +获取当前路由的实例,等同于 `app.router`。 + +- 类型 + +```tsx | pure +const useRouter: () => RouterManager +``` + +- 示例 + +```tsx | pure +import { useRouter } from '@nocobase/client'; + +const Demo = () => { + const router = useRouter(); +} +``` diff --git a/packages/core/client/docs/en-US/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png b/packages/core/client/docs/en-US/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png new file mode 100644 index 0000000000..0fa567bf5f Binary files /dev/null and b/packages/core/client/docs/en-US/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png differ diff --git a/packages/core/client/docs/en-US/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png b/packages/core/client/docs/en-US/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png new file mode 100644 index 0000000000..d27e675f70 Binary files /dev/null and b/packages/core/client/docs/en-US/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png differ diff --git a/packages/core/client/docs/en-US/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png b/packages/core/client/docs/en-US/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png new file mode 100644 index 0000000000..9d65a782cb Binary files /dev/null and b/packages/core/client/docs/en-US/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png differ diff --git a/packages/core/client/docs/en-US/core/static/designer.png b/packages/core/client/docs/en-US/core/static/designer.png new file mode 100644 index 0000000000..466fa4cfd8 Binary files /dev/null and b/packages/core/client/docs/en-US/core/static/designer.png differ diff --git a/packages/core/client/docs/en-US/core/static/plugin-settings.jpg b/packages/core/client/docs/en-US/core/static/plugin-settings.jpg new file mode 100644 index 0000000000..34a8da1162 Binary files /dev/null and b/packages/core/client/docs/en-US/core/static/plugin-settings.jpg differ diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx new file mode 100644 index 0000000000..40a0c5a297 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-basic.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-common.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-common.tsx new file mode 100644 index 0000000000..79962078e3 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-common.tsx @@ -0,0 +1,38 @@ +import { ApplicationOptions, Grid, Plugin, SchemaComponent } from '@nocobase/client'; +import React from 'react'; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid }); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const appOptions: ApplicationOptions = { + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}; + +export { appOptions }; diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx new file mode 100644 index 0000000000..6427050686 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-component.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { Avatar } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + Component: (props: any) => ( + + C + + ), + componentProps: { + size: 'large', + }, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-divider.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-divider.tsx new file mode 100644 index 0000000000..39a23be52e --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-divider.tsx @@ -0,0 +1,57 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'd', + type: 'divider', + }, + { + name: 'b', + title: 'B Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'B1', + }, + { + name: 'a2', + type: 'item', + title: 'B2', + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx new file mode 100644 index 0000000000..d691ff9259 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-group.tsx @@ -0,0 +1,71 @@ +import { Application, SchemaInitializer, SchemaInitializerItemGroup } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; +import React from 'react'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'b', + title: 'B Group Title', + type: 'itemGroup', + divider: true, // 渲染分割线 + useChildren() { + // 动态子元素 + return [ + { + name: 'b1', + type: 'item', + title: 'B1', + }, + { + name: 'b2', + type: 'item', + title: 'B2', + }, + ]; + }, + }, + { + name: 'c', + Component: () => { + return ( + + {[ + { + name: 'c1', + type: 'item', + title: 'C1', + }, + ]} + + ); + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-item.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-item.tsx new file mode 100644 index 0000000000..825e81586a --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-item.tsx @@ -0,0 +1,88 @@ +/** + * defaultShowCode: true + */ +import { + Grid, + SchemaInitializer, + useSchemaInitializer, + SchemaInitializerItem, + CardItem, + Application, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; +import { useFieldSchema } from '@formily/react'; + +const Demo = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + Component: Demo, + }, + { + name: 'demo2', + type: 'item', + useComponentProps() { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + + return { + title: 'type 方式定义', + onClick: handleClick, + }; + }, + }, + { + name: 'demo3', + title: 'with items', + type: 'item', + onClick(args) { + console.log(args); + }, + items: [ + { + label: 'aaa', + value: 'aaa', + }, + { + label: 'bbb', + value: 'bbb', + }, + ], + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], + components: { CardItem, Hello }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx new file mode 100644 index 0000000000..f795b02715 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-menu.tsx @@ -0,0 +1,53 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A subMenu', + type: 'subMenu', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'b', + title: 'B subMenu', + type: 'subMenu', + children: [ + { + name: 'a1', + type: 'item', + title: 'B1', + }, + { + name: 'a2', + type: 'item', + title: 'B2', + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx new file mode 100644 index 0000000000..b635052e30 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-select.tsx @@ -0,0 +1,53 @@ +/** + * defaultShowCode: true + */ +import { useFieldSchema } from '@formily/react'; +import { Application, Grid, SchemaInitializer, SchemaInitializerSelect, useDesignable } from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; + +const OpenModeSelect = () => { + const fieldSchema = useFieldSchema(); + const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer'; + + const { patch } = useDesignable(); + const handleChange = (value) => { + // 修改当前节点的 Schema 的属性 + patch({ + 'x-component-props': { + openMode: value, + }, + }); + }; + + return ( + + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'openMode', + Component: OpenModeSelect, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-switch.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-switch.tsx new file mode 100644 index 0000000000..c8eb33888d --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-components-switch.tsx @@ -0,0 +1,88 @@ +/** + * defaultShowCode: true + */ +import { + Grid, + SchemaInitializer, + Application, + SchemaInitializerSwitch, + useCurrentSchema, + useSchemaInitializer, + Action, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; + +const actionKey = 'x-action'; + +const schema = { + type: 'void', + [actionKey]: 'create', + title: "{{t('Add new')}}", + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + }, +}; + +const AddNewButton = () => { + // 判断是否已插入 + const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey); + + const { insert } = useSchemaInitializer(); + return ( + { + // 如果已插入,则移除 + if (exists) { + return remove(); + } + // 新插入子节点 + insert(schema); + }} + /> + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Configure actions', + wrap: Grid.wrap, + items: [ + { + name: 'Add New', + Component: AddNewButton, + }, + { + name: 'Add New2', + type: 'switch', + useComponentProps() { + const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey); + + const { insert } = useSchemaInitializer(); + return { + checked: exists, + title: 'Add new - type 方式', + onClick() { + // 如果已插入,则移除 + if (exists) { + return remove(); + } + // 新插入子节点 + insert(schema); + }, + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], + components: { Action }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-hooks-item.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-hooks-item.tsx new file mode 100644 index 0000000000..ddf54ed2d4 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-hooks-item.tsx @@ -0,0 +1,33 @@ +/** + * defaultShowCode: true + */ +import React, { ReactNode } from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem, useSchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; +import { TableOutlined } from '@ant-design/icons'; + +const Demo = () => { + const { name, foo, icon } = useSchemaInitializerItem<{ name: string; foo: string; icon: ReactNode }>(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + foo: 'bar', + icon: , + Component: Demo, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx new file mode 100644 index 0000000000..9c7ae48c33 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-items.tsx @@ -0,0 +1,143 @@ +import React, { FC } from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerChild, + SchemaInitializerItem, + SchemaInitializerItemsProps, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { ButtonProps, ListProps, List, Card } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const CustomListGridMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ( + + + + )} + > + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + ItemsComponent: CustomListGridMenu, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-children.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-children.tsx new file mode 100644 index 0000000000..1e7fe3e234 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-children.tsx @@ -0,0 +1,61 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + type: 'itemGroup', + title: '静态 children', + children: [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ], + }, + { + name: 'a', + type: 'itemGroup', + title: '动态 children', + useChildren() { + return [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ]; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-define.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-define.tsx new file mode 100644 index 0000000000..534ec581b3 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-define.tsx @@ -0,0 +1,34 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + }, + { + name: 'b', + type: 'item', // 通过 `type` 定义,底层对应着 `SchemaInitializerItem` 组件 + title: 'type Demo', + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-props.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-props.tsx new file mode 100644 index 0000000000..37d46fa4ed --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-props.tsx @@ -0,0 +1,40 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const CommonDemo = (props) => { + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + Component: CommonDemo, + componentProps: { + title: 'componentProps', + }, + }, + { + name: 'b', + Component: CommonDemo, + useComponentProps() { + return { + title: 'useComponentProps', + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-visible.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-visible.tsx new file mode 100644 index 0000000000..ead52953d1 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-options-item-visible.tsx @@ -0,0 +1,32 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + type: 'item', + title: 'Demo A', + }, + { + name: 'b', + type: 'item', + title: 'Demo B', + useVisible() { + return false; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx new file mode 100644 index 0000000000..a5890ba9fb --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-popover.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + useDesignable, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { Button } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +const MyInitializerComponent = () => { + const { insertBeforeEnd } = useDesignable(); + return ( + + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + popover: false, + Component: MyInitializerComponent, +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-render.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-render.tsx new file mode 100644 index 0000000000..214a17cec8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-initializer-render.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return ( +
+
{render()}
+
可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}
+
+ ); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-common.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-common.tsx new file mode 100644 index 0000000000..01b99b9aaa --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-common.tsx @@ -0,0 +1,48 @@ +import { ApplicationOptions, CardItem, Grid, Plugin, SchemaComponent } from '@nocobase/client'; +import React from 'react'; + +const Hello = () => { + return

Hello, world!

; +}; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const appOptions: ApplicationOptions = { + router: { + type: 'memory', + }, + designable: true, + components: { Grid, CardItem, Hello }, + plugins: [PluginHello], +}; + +export { appOptions }; diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-action-modal.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-action-modal.tsx new file mode 100644 index 0000000000..1a7076c923 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-action-modal.tsx @@ -0,0 +1,66 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { + Application, + FormItem, + Input, + SchemaSettings, + SchemaSettingsActionModalItem, + useDesignable, +} from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { ISchema, useField } from '@formily/react'; + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const filed = useField(); + const { patch } = useDesignable(); + + return ( + { + patch({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ FormItem, Input }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-divider.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-divider.tsx new file mode 100644 index 0000000000..f2b3d98b66 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-divider.tsx @@ -0,0 +1,68 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'group A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'b', + type: 'itemGroup', + componentProps: { + title: 'group B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-group.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-group.tsx new file mode 100644 index 0000000000..e47b99a135 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-group.tsx @@ -0,0 +1,64 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'group A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'b', + type: 'itemGroup', + componentProps: { + title: 'group B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-item.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-item.tsx new file mode 100644 index 0000000000..376ff77018 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-item.tsx @@ -0,0 +1,76 @@ +/** + * defaultShowCode: true + */ +import React, { FC, useState } from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Button, Input, Space } from 'antd'; + +const MarkdownEdit = () => { + const field = useField(); + return ( + { + field.editable = true; + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'markdown', + Component: MarkdownEdit, + }, + ], +}); + +const Hello: FC<{ content?: string }> = observer((props) => { + const field = useField(); + const { content } = props; + const [inputVal, setInputVal] = useState(content); + const { patch } = useDesignable(); + return field.editable ? ( + + setInputVal(e.target.value)} /> + + + + + + ) : ( +

{content}

+ ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-modal.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-modal.tsx new file mode 100644 index 0000000000..2f35e88872 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-modal.tsx @@ -0,0 +1,66 @@ +/** + * defaultShowCode: true + */ +import { ISchema } from '@formily/react'; +import { + Application, + FormItem, + Input, + SchemaSettings, + SchemaSettingsModalItem, + useSchemaSettings, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-settings-common'; + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const { dn } = useSchemaSettings(); + + return ( + { + dn.deepMerge({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ FormItem, Input }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-remove.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-remove.tsx new file mode 100644 index 0000000000..c1b101cc30 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-remove.tsx @@ -0,0 +1,28 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn(s) { + return s['x-component'] === 'Grid'; // 其顶级是 Grid,这一层级不能删 + }, + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-select.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-select.tsx new file mode 100644 index 0000000000..c14dfaff40 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-select.tsx @@ -0,0 +1,82 @@ +/** + * defaultShowCode: true + */ +import React, { FC } from 'react'; +import { Application, SchemaSettings, SchemaSettingsSelectItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Table } from 'antd'; + +const PageSize = () => { + const { patch } = useDesignable(); + const filed = useField(); + return ( + { + patch({ + 'x-component-props': { + pageSize: v, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'pageSize', + Component: PageSize, + }, + ], +}); + +const data = []; +for (let i = 0; i < 46; i++) { + data.push({ + key: i, + name: `Edward King ${i}`, + age: 32, + address: `London, Park Lane no. ${i}`, + }); +} +const Hello: FC<{ pageSize: number }> = observer((props) => { + return ( + + ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-sub-menu.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-sub-menu.tsx new file mode 100644 index 0000000000..4e9be9478c --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-sub-menu.tsx @@ -0,0 +1,64 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'subMenu', + componentProps: { + title: 'Sub Menu A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'b', + type: 'subMenu', + componentProps: { + title: 'Sub Menu B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-switch.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-switch.tsx new file mode 100644 index 0000000000..7569ecddf3 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-components-switch.tsx @@ -0,0 +1,74 @@ +/** + * defaultShowCode: true + */ +import React, { FC } from 'react'; +import { Application, SchemaSettings, SchemaSettingsSwitchItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Form, Input } from 'antd'; + +const FormItemRequired = () => { + const { patch } = useDesignable(); + const filed = useField(); + return ( + { + patch({ + 'x-component-props': { + required: v, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'required', + Component: FormItemRequired, + }, + { + name: 'required2', + type: 'switch', + useComponentProps() { + const { patch } = useDesignable(); + const filed = useField(); + return { + title: 'Required - type 方式', + checked: !!filed.componentProps?.required, + onChange(v) { + patch({ + 'x-component-props': { + required: v, + }, + }); + }, + }; + }, + }, + ], +}); + +const Hello: FC<{ required: boolean }> = observer((props) => { + return ( +
+ + + + + ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx new file mode 100644 index 0000000000..45fba8c3ba --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-children.tsx @@ -0,0 +1,68 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: '静态 children', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A 2', + }, + }, + ], + }, + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: '动态 children', + }, + useChildren() { + return [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ]; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-define.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-define.tsx new file mode 100644 index 0000000000..f370a23ee1 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-define.tsx @@ -0,0 +1,35 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return ; +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + }, + { + name: 'b', + type: 'item', // 通过 `type` 定义,底层对应着 `SchemaInitializerItem` 组件 + componentProps: { + title: 'type Demo', + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-props.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-props.tsx new file mode 100644 index 0000000000..7b37376111 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-props.tsx @@ -0,0 +1,39 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const CommonDemo = (props) => { + return ; +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: CommonDemo, + componentProps: { + title: 'componentProps', + }, + }, + { + name: 'b', + Component: CommonDemo, + useComponentProps() { + return { + title: 'useComponentProps', + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-visible.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-visible.tsx new file mode 100644 index 0000000000..6cfc7f725f --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-options-item-visible.tsx @@ -0,0 +1,35 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'item', + componentProps: { + title: 'Demo A', + }, + }, + { + name: 'b', + type: 'item', + componentProps: { + title: 'Demo B', + }, + useVisible() { + return false; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx new file mode 100644 index 0000000000..cb179a78a5 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/demos/schema-settings-render.tsx @@ -0,0 +1,151 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + FormItem, + Grid, + Input, + Plugin, + SchemaComponent, + SchemaSettings, + SchemaSettingsModalItem, + createDesignable, + useAPIClient, + useSchemaComponentContext, + useSchemaSettings, + useSchemaSettingsRender, +} from '@nocobase/client'; +import React, { useMemo } from 'react'; + +class PluginHello extends Plugin { + async load() { + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const { dn } = useSchemaSettings(); + console.log(dn.current); + + return ( + { + dn.shallowMerge({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const Hello = (props) => { + return ( +

+ Hello, world! + {props.children} +

+ ); +}; + +const Demo = () => { + const fieldSchema = useFieldSchema(); + const field = useField(); + const api = useAPIClient(); + const { refresh } = useSchemaComponentContext(); + const dn = useMemo( + () => + createDesignable({ + current: fieldSchema.parent, + model: field.parent, + api, + refresh, + }), + [], + ); + const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], { + fieldSchema: dn.current, + field: dn.model, + dn, + }); + return ( +
+
{render()}
+
可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}
+
+ ); +}; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +const app = new Application({ + schemaSettings: [mySettings], + router: { + type: 'memory', + }, + designable: true, + components: { FormItem, Input, Grid, CardItem, Hello }, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/en-US/core/ui-schema/designable.md b/packages/core/client/docs/en-US/core/ui-schema/designable.md new file mode 100644 index 0000000000..75b151b260 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/designable.md @@ -0,0 +1,791 @@ +# Designable + +## Designable + +对 Schema 节点进行增、删、改操作,并且提供了事件触发机制,用于将数据同步到服务端。 + +```tsx | pure +interface Options { + current: Schema; + api?: APIClient; + onSuccess?: any; + refresh?: () => void; + t?: any; +} + +interface InsertAdjacentOptions { + wrap?: (s: ISchema) => ISchema; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | BreakFn; + onSuccess?: any; +} + +class Designable { + constructor(options: Options ) { } + loadAPIClientEvents(): void; + on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', listener: any): void + emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', ...args: any[]): Promise + refresh(): void + recursiveRemoveIfNoChildren(schema?: Schema, options?: RecursiveRemoveOptions): Schema; + remove(schema?: Schema, options?: RemoveOptions): Promise + removeWithoutEmit(schema?: Schema, options?: RemoveOptions): Schema + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void | Promise + insertBeforeBeginOrAfterEnd(schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeBegin(schema: ISchema, options?: InsertAdjacentOptions): void + insertAfterBegin(schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeEnd(schema: ISchema, options?: InsertAdjacentOptions): Promise + insertAfterEnd(schema: ISchema, options?: InsertAdjacentOptions): void +} +``` + +### 构造函数 + +- 参数讲解 + + - `current`:需要操作的 Schema 节点 + - `api`:用于发起后端请求的 [APIClient](https://docs.nocobase.com/api/sdk) 实例 + - `onSuccess`:后端接口请求成功后的回调 + - `refresh`:用于更新节点后,刷新页面 + - `t`:`useTranslation()` 的返回值 +- 示例 + +```tsx | pure +const schema = new Schema({ + type: 'void', + name: 'hello', + 'x-component': 'div', +}) + +const dn = new Designable({ current: schema }); +``` + +### Schema 操作方法 + +```tsx | pure +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + } + } +}); + +const b = schema.b; + +const dn = createDesignable({ + current: b, +}) + +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +#### remove + +移除当前节点 + +```tsx | pure +dn.remove(); +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.remove(); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { +- b: { +- type: 'void', +- properties: { +- c: { +- type: 'void', +- } +- } +- } + } +} +``` + +#### insertBeforeBegin + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertBeforeBegin({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertBeforeBegin({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { ++ d: { ++ type: 'void', ++ }, + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + } + } +} +``` + +#### insertAfterBegin + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertAfterBegin({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertAfterBegin({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { ++ d: { ++ type: 'void', ++ }, + c: { + type: 'void', + } + } + } + } +} +``` + +#### insertBeforeEnd + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertBeforeEnd({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertBeforeEnd({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, ++ d: { ++ type: 'void', ++ }, + } + } + } +} +``` + +#### insertAfterEnd + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertAfterEnd({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertAfterEnd({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + }, ++ d: { ++ type: 'void', ++ }, + } +} +``` + +#### insertAdjacent + +根据第一个参数决定插入的位置,是前面四个方法的封装。 + +```tsx | pure +class Designable { + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void | Promise +} +``` + +### 事件监听和 API 请求 + +- `on` :添加事件监听的基础方法 +- `loadAPIClientEvents`:调用 `on` 方法添加对 `insertAdjacent`、`patch`、`batchPatch`、`remove` 的事件的监听,主要功能是将变更的 Schema 更新到服务端 +- `emit`:是根据事件名称,调用之前注册过的方法,具体是由前面讲过的 *插入操作和删除操作* 触发 + +而 `loadAPIClientEvents()` 并非在初始化时调用,需要手动调用,换而言之,如果不调用 `dn.loadAPIClientEvents()`,则不会将更新发送到服务端,主要是简化在单测或者 DEMO 环境对服务端的 Mock。 + +## 工具函数 + +### createDesignable() + +对 `new Designable()` 的简单封装。 + +```tsx | pure +function createDesignable(options: CreateDesignableProps) { + return new Designable(options); +} +``` + +```tsx | pure +const dn = createDesignable({ current: schema }); +``` + +## Hooks + +### useFieldSchema() + +用户获取当前节点 Schema JSON 对象,更多信息请参考 [formily useFieldSchema()](https://react.formilyjs.org/api/hooks/use-field-schema)。 + +- 类型 + +```tsx | pure +import { Schema } from '@formily/json-schema'; +const useFieldSchema: () => Schema; +``` + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import React from 'react'; +import { useFieldSchema } from '@formily/react'; +import { Application, Plugin, SchemaComponent } from '@nocobase/client'; +const Demo = ({ children }) => { + const fieldSchema = useFieldSchema(); + return
+
{ JSON.stringify(fieldSchema, null, 2)}
+
{children}
+
+} +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Demo', // 这里是 Demo 组件 + 'properties': { + 'world': { + 'type': 'void', + 'x-component': 'Demo', // 这里也是 Demo 组件 + }, + } +} + +const Root = () => { + return +} + +const app = new Application({ + providers: [Root] +}) + +export default app.getRootComponent(); +``` + +### useField() + +获取当前节点 Schema 实例,更多信息请参考 [formily useField()](https://react.formilyjs.org/api/hooks/use-field) + +- 类型 + +```tsx | pure +import { GeneralField } from '@formily/core'; +const useField: () => T; +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const field = useField(); + console.log('field', field); + return
+} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Demo', // 这里是 Demo 组件 + 'properties': { + 'world': { + 'type': 'void', + 'x-component': 'Demo', // 这里也是 Demo 组件 + }, + } +} + +const Root = () => { + return +} +``` + +### useDesignable() + +对当前 Schema 节点的修改操作。 + +- 类型 + +```tsx | pure +interface InsertAdjacentOptions { + wrap?: (s: ISchema) => ISchema; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | BreakFn; + onSuccess?: any; +} + +type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; + +function useDesignable(): { + dn: Designable; + designable: boolean; + reset: () => void; + refresh: () => void; + setDesignable: (value: boolean) => void; + findComponent(component: any): any; + patch: (key: ISchema | string, value?: any) => void + on(name: "error" | "insertAdjacent" | "remove" | "patch" | "batchPatch", listener: any): void + remove(schema?: any, options?: RemoveOptions): void + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeBegin(schema: ISchema): void; + insertAfterBegin(schema: ISchema): void; + insertBeforeEnd(schema: ISchema): void; + insertAfterEnd(schema: ISchema): void; +} +``` + +- 详细解释 + + - designable、reset、refresh、setDesignable:这些值继承自 [SchemaComponentContext](https://www.baidu.com) + - dn:是 `Designable` 的实例 + - findComponent:用于查找 Schema 中字符串对应真正的组件,如果组件未注册则返回 `null` + - remove:内部调用的是 `dn.remove` 方法 + - on:内部调用的是 `dn.on` 方法 + - insertAdjacent:插入新的 Schema 节点,内部调用的是 `dn.insertAdjacent` 方法 + - position:插入位置 + - schema:新的 Schema 节点 + - Options + - wrap:对 Schema 的二次处理的回调函数 + - removeParentsIfNoChildren:当没有子元素时,删除父元素 + - breakRemoveOn:停止删除的判断回调 + - onSuccess:插入成功的回调 + - insertBeforeBegin:内部调用的是 `dn.insertBeforeBegin` 方法 + - insertAfterBegin:内部调用的是 `dn.insertAfterBegin` 方法 + - insertBeforeEnd:内部调用的是 `dn.insertBeforeEnd` 方法 + - insertAfterEnd:内部调用的是 `dn.insertAfterEnd` 方法 +- 示例 + +插入节点。 + +```tsx +import React from 'react'; +import { + SchemaComponentProvider, + SchemaComponent, + useDesignable, +} from '@nocobase/client'; +import { observer, Schema, useFieldSchema } from '@formily/react'; +import { Button, Space } from 'antd'; +import { uid } from '@formily/shared'; + +const Hello = observer( + (props) => { + const { insertAdjacent } = useDesignable(); + const fieldSchema = useFieldSchema(); + return ( +
+

{fieldSchema.name}

+ + + + + + +
{props.children}
+
+ ); + }, + { displayName: 'Hello' }, +); + +const Page = observer( + (props) => { + return
{props.children}
; + }, + { displayName: 'Page' }, +); + +export default () => { + return ( + + + + ); +}; +``` + +部分更新。 + +```tsx +import React from 'react'; +import { + SchemaComponentProvider, + SchemaComponent, + useDesignable, +} from '@nocobase/client'; +import { observer, Schema, useField, useFieldSchema } from '@formily/react'; +import { FormItem } from '@formily/antd-v5'; +import { Button } from 'antd'; +import { uid } from '@formily/shared'; + +const Hello = observer( + (props) => { + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + return ( +
+

{field.title}

+ { JSON.stringify(props) } +
+ { JSON.stringify(field.componentProps) } +
+ { JSON.stringify(field.decoratorProps) } +
+ { JSON.stringify(fieldSchema.toJSON()) } +
+ + +
+ ); + }, + { displayName: 'Hello' }, +); + +const Page = observer( + (props) => { + return
{props.children}
; + }, + { displayName: 'Page' }, +); + +export default () => { + return ( + + + + ); +}; +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-component.md b/packages/core/client/docs/en-US/core/ui-schema/schema-component.md new file mode 100644 index 0000000000..70c16cfe02 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-component.md @@ -0,0 +1,208 @@ +# SchemaComponent + +## Context + +### SchemaComponentContext + +```tsx | pure +interface SchemaComponentContext { + scope?: any; + components?: SchemaReactComponents; + refresh?: () => void; + reset?: () => void; + designable?: boolean; + setDesignable?: (value: boolean) => void; +} +``` + +Schema 渲染的上下文。 + +- `scope`:Schema 中变量的映射 +- `components`: Schema 中组件的映射 +- `refresh`:触发 React 重新渲染的工具函数 +- `reset`:重置整个 Schema 节点 +- `designable`:是否显示设计器,默认 `false` +- `setDesignable`:用于切换 `designable` 的值 + +## Hooks + +### useSchemaComponentContext() + +用于获取 `SchemaComponentContext` 的值,是 `useContext(SchemaComponentContext)` 的封装。 + +## 组件 + +### SchemaComponentProvider + +其是对 `SchemaComponentContext.Provider` 和 [FormProvider ](https://react.formilyjs.org/api/components/form-provider)的封装,并内置在 `Application` 中,并且会将 `app.components` 和 `app.scopes` 传递过去,所以一般情况下 *不需要关注* 此组件。 + +- props + +```tsx | pure +interface SchemaComponentProviderProps { + designable?: boolean; + form?: Form; + scope?: any; + components?: SchemaReactComponents; +} +``` + +- 详细解释 + - `designable`:`SchemaComponentContext` 中 `designable` 的默认值 + - `form`:NocoBase 的 Schema 能力是基于 formily 的 `FormProvider` 提供的,form 是其参数,默认为 `createForm()` + - `scope`:Schema 中所用到的变量,会通过 `SchemaComponentContext` 进行传递 + - `components`:Schema 中所用到的组件,会通过 `SchemaComponentContext` 进行传递 + +### SchemaComponent + +用于渲染 Schema,此组件必须和 `SchemaComponentProvider` 一起使用,因为 `SchemaComponentProvider` 提供了 [FormProvider](https://react.formilyjs.org/api/components/form-provider) 作为渲染 Schema 的根节点。 + +- Props + +```tsx | pure +type SchemaComponentProps = (ISchemaFieldProps | IRecursionFieldProps) & { + memoized?: boolean; + components?: SchemaReactComponents; + scope?: any; +} +``` + +- 详细解释 + + - `memoized`:当为 `true` 时,会对每层的 Schema 使用 `useMemo()` 进行处理 + - `components`:同 `SchemaComponentProvider` 的 `components` + - `scope`: 同 `SchemaComponentProvider` 的 `components` + +## 综合示例 + +结合 `SchemaComponentProvider`、 `useSchemaComponentContext()` 和 `SchemaComponent`。 + +```tsx +/** + * defaultShowCode: true + */ +import { SchemaComponentProvider, useSchemaComponentContext, SchemaComponent, } from '@nocobase/client'; +const Hello = () => { + const { designable, setDesignable } = useSchemaComponentContext(); + return
+
hello world
+ +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', +} + +const Demo = () => { + return +} + +const Root = () => { + return + + +} + +export default Root; +``` + +使用 `new Application()` 的方式,其内置了 `SchemaComponentProvider` ,我们可以如下操作: + +```tsx +/** + * defaultShowCode: true + */ +import { Application, Plugin, useSchemaComponentContext, SchemaComponent } from '@nocobase/client'; +const Hello = () => { + const { designable, setDesignable } = useSchemaComponentContext(); + return
+
hello world
+ +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', +} + +const HomePage = () => { + return +} + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ Hello }); + this.app.router.add('home', { + path: '/', + Component: HomePage, + }) + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}) + +export default app.getRootComponent(); +``` + +### SchemaComponentOptions + +在应用中,会有很多层级的嵌套,每一层都可能提供自己的组件和 scope,此组件就是为了层层传递 Schema 所需的 `components` 和 `scope` 的。 + +- props + +```tsx | pure +interface SchemaComponentOptionsProps { + scope?: any; + components?: SchemaReactComponents; +} +``` + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import { SchemaComponentProvider, useSchemaComponentContext, SchemaComponent, SchemaComponentOptions } from '@nocobase/client'; +const World = () => { + return
world
+} + +const Hello = ({ children }) => { + return
+
hello
+ { children } +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', + properties: { + world: { + type: 'void', + 'x-component': 'World', + }, + }, +} + +const Root = () => { + return + + +} + +export default Root; +``` diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-initializer-manager.md b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer-manager.md new file mode 100644 index 0000000000..483f9d2b6c --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer-manager.md @@ -0,0 +1,186 @@ +# SchemaInitializerManager + +## 实例方法 + +### schemaInitializerManager.add() + +添加 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + add(...schemaInitializerList: SchemaInitializer[]): void +} +``` + +- 示例 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add block', + items: [ + { + name: 'demo', + type: 'item', + title: 'Demo' + } + ], +}); + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + } +} +``` + +### schemaInitializerManager.get() + +获取一个 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + get(name: string): SchemaInitializer | undefined +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + } +} +``` + +### schemaInitializerManager.getAll() + +获取所有的 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + getAll(): Record> +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const list = this.app.schemaInitializerManager.getAll(); + } +} +``` + +### app.schemaInitializerManager.has() + +判断是否有存在某个 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + has(name: string): boolean +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasMyInitializer = this.app.schemaInitializerManager.has('MyInitializer'); + } +} +``` + +### schemaInitializerManager.remove() + +移除 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + remove(name: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.remove('MyInitializer'); + } +} +``` + +### schemaInitializerManager.addItem() + +添加 SchemaInitializer 实例的 Item 项,其和直接 schemaInitializer.add() 方法的区别是,可以确保在实例存在时才会添加。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + addItem(schemaInitializerName: string, itemName: string, data: Omit): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再添加子项,需要确保已注册 + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + if (myInitializer) { + myInitializer.add('b', { type: 'item', title: 'B' }) + } + + // 方式2:通过 addItem,内部确保在 MyInitializer 注册时才会添加 + this.app.schemaInitializerManager.addItem('MyInitializer', 'b', { + type: 'item', + title: 'B' + }) + } +} +``` + +### schemaInitializerManager.removeItem() + +移除 实例的 Item 项,其和直接 schemaInitializer.remove() 方法的区别是,可以确保在实例存在时才会移除。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + removeItem(schemaInitializerName: string, itemName: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再删除子项,需要确保已注册 + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + if (myInitializer) { + myInitializer.remove('a') + } + + // 方式2:通过 addItem,内部确保在 MyInitializer 注册时才会移除 + this.app.schemaInitializerManager.remove('MyInitializer', 'a') + } +} +``` diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md new file mode 100644 index 0000000000..2637b0a80b --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-initializer.md @@ -0,0 +1,704 @@ +# SchemaInitializer + +## new SchemaInitializer(options) + +```tsx | pure +interface SchemaInitializerOptions { + Component?: ComponentType; + componentProps?: P1; + style?: React.CSSProperties; + title?: string; + icon?: ReactNode; + + items?: SchemaInitializerItemType[]; + ItemsComponent?: ComponentType; + itemsComponentProps?: P2; + itemsComponentStyle?: React.CSSProperties; + + insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; + designable?: boolean; + wrap?: (s: ISchema) => ISchema; + onSuccess?: (data: any) => void; + insert?: InsertType; + useInsert?: () => InsertType; + + popover?: boolean; + popoverProps?: PopoverProps; +} + +class SchemaInitializer { + constructor(options: SchemaInitializerOptions & { name: string }): SchemaInitializer; + add(name: string, item: Omit): void + get(nestedName: string): SchemaInitializerItemType | undefined + remove(nestedName: string): void +} +``` + +### 详细解释 + +![](../static/KTUWb69kioUg8bxYTAMc2ReDnRg.png) + +- name:唯一标识,必填 +- Component 相关 + + - Component:触发组件,默认是 `Button` 组件 + - componentProps: 组件属性,默认是 `ButtonProps` + - title: 按钮的文本 + - icon:按钮的 icon 属性 + - style:组件的样式 +- Items 相关 + + - items:列表项配置 + - ItemsComponent:默认是渲染成一个列表的形式,可通过此参数自定义 items + - itemsComponentProps:`ItemsComponent` 的属性 + - itemsComponentStyle:`ItemsComponent` 的样式 +- popover 组件相关 + + - popover:是否使用 popover,默认为 `true` + - popoverProps:popover 的属性 +- Schema 操作相关 + + - insertPosition:插入位置,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - designable:是否显示设计模式,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - wrap:对 Schema 的二次处理,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - onSuccess:Schema 更新到服务端后的回调,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - insert:自定义 Schema 插入逻辑,默认为 [useDesignable()](/core/ui-schema/designable#usedesignable) 的 `insertAdjacent` + - useInsert:当自定义插入 Schema 的逻辑需要用到 Hooks 时,可以使用此参数 + +### 示例 + +#### 基础用法 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + ], +}); +``` + + + + +#### 定制化 `Component` + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + Component: (props) => ( + + C + + ), + componentProps: { + size: 'large', + }, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + } + ], +}); +``` + + + +#### 不使用 Popover + +关于 `useDesignable()` 的说明请参考 [useDesignable](/core/ui-schema/designable#usedesignable)。 + +```tsx | pure +const schema = { + type: 'void', + title: Math.random(), + 'x-component': 'Hello', +}; +const MyInitializerComponent = () => { + const { insertBeforeEnd } = useDesignable(); + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add block', + popover: false, + Component: MyInitializerComponent, +}); +``` + + + + +#### 定制化 Items + +```tsx | pure +const CustomListGridMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ( + + + + )} + > + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + ItemsComponent: CustomListGridMenu, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + } + ], +}); +``` + + + +## options.items 配置详解 + +### 类型 + +```tsx | pure +interface SchemaInitializerComponentCommonProps { + title?: string; + schema?: ISchema; + style?: React.CSSProperties; + className?: string; +} + +interface SchemaInitializerItemBaseType extends SchemaInitializerComponentCommonProps { + name: string; + sort?: number; + type?: string; + Component?: string | ComponentType; + componentProps?: Omit; + useComponentProps?: () => Omit; + useVisible?: () => boolean; + children?: SchemaInitializerItemType[]; + useChildren?: () => SchemaInitializerItemType[]; + [index: string]: any; +} +``` + +### 两种定义方式:`Component` 和 `type` + + +- 通过 `Component` 定义 + +```tsx | pure + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + } + ], +}); +``` + +- 通过 `type` 定义 + +NocoBase 内置了一些常用的 `type`,例如 `type: 'item'`,相当于 `Component: SchemaInitializerItem`。 + +更多内置类型,请参考:[内置组件和类型](/core/ui-schema/schema-initializer#%E5%86%85%E7%BD%AE%E7%BB%84%E4%BB%B6%E5%92%8C%E7%B1%BB%E5%9E%8B) + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + items: [ + { + name: 'a', + type: 'item', + title: 'Demo' + } + ], +}); +``` + + + +### `children` 和动态方式 `useChildren` + +对于某些组件而言是有子列表项的,例如 `type: 'itemGroup'`,那么我们使用 children 属性,同时考虑到某些场景下 children 是动态的,需要从 Hooks 里面获取,那么就可以通过 `useChildren` 来定义。 + + + +### 动态显示隐藏 `useVisible` + + + +### 组件属性 `componentProps` 和动态属性 `useComponentProps` + +对于一些通用组件,我们可以通过 `componentProps` 来定义组件属性,同时考虑到某些场景下组件属性是动态的,需要从 Hooks 里面获取,那么就可以通过 `useComponentProps` 来定义。 + +当然也可以不使用这两个属性,直接封装成一个组件,然后通过 `Component` 来定义。 + + + +### 公共属性和组件属性 + +```tsx | pure +{ + name: 'demo', + title: 'Demo', + foo: 'bar', + Component: Demo, + componentProps: { + zzz: 'xxx', + }, +} +``` + +从上面的示例中我么看到,从配置项中获取组件组件所需的数据有两个方式: + +- 组件属性:通过 `componentProps` 来定义,例如 `zzz: 'xxx'` +- 公共属性:将属性直接定义在配置项上,例如 `foo: 'bar'`、`name`、`title` + +在获取上 + +- `componentProps` 定义的数据会被传递给组件的 `props` +- 直接定义在配置项上的数据会则需要通过 [useSchemaInitializerItem()](/core/ui-schema/schema-initializer#useschemainitializeritem) 获取 + +```tsx | pure +const Demo = (props) => { + console.log(props); // { zzz: 'xxx' } + const { foo } = useSchemaInitializerItem(); // { foo: 'bar' } +} +``` + +## 实例方法 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + } + ], + }, + ], +}); +``` + +```tsx +import { SchemaInitializer, Application, useSchemaInitializerRender } from '@nocobase/client'; +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + type: 'item', + } + ], + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return render(); +} +const app = new Application({ + schemaInitializers: [myInitializer], + providers: [Root], + designable: true, +}); + +export default app.getRootComponent(); +``` + +### schemaInitializer.add() + +用于新增 Item,另一种添加方式参考 [schemaInitializerManager.addItem()](/core/ui-schema/schema-initializer-manager#schemainitializermanageradditem); + +- 类型 + +```tsx | pure +class SchemaInitializer { + add(name: string, item: Omit): void +} +``` + +- 参数说明 + +第一个参数是 name,作为唯一标识,用于增删改查,并且 `name` 支持 `.` 用于分割层级。 + +- 示例 + +```tsx | pure +myInitializer.add('b', { + type: 'item', + title: 'item b', +}) + +myInitializer.add('a.a2', { + type: 'item', + title: 'item a2', +}) +``` + +```tsx +import { SchemaInitializer, Application, useSchemaInitializerRender } from '@nocobase/client'; +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + type: 'item', + } + ], + }, + ], +}); + +myInitializer.add('b', { + type: 'item', + title: 'item b', +}) + +myInitializer.add('a.a2', { + type: 'item', + title: 'item a2', +}) + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return render(); +} +const app = new Application({ + schemaInitializers: [myInitializer], + providers: [Root], + designable: true, +}); + + +export default app.getRootComponent(); +``` + + +### schemaInitializer.get() + +- 类型 + +```tsx | pure +class SchemaInitializer { + get(nestedName: string): SchemaInitializerItemType | undefined +} +``` + +- 示例 + +```tsx | pure +const itemA = myInitializer.get('a') + +const itemA1 = myInitializer.add('a.a1') +``` + +### schemaInitializer.remove() + +另一种移除方式参考 [schemaInitializerManager.addItem()](/core/ui-schema/schema-initializer-manager#schemainitializermanagerremoveitem); + +- 类型 + +```tsx | pure +class SchemaInitializer { + remove(nestedName: string): void +} +``` + +- 示例 + +```tsx | pure +myInitializer.remove('a.a1') + +myInitializer.remove('a') +``` + +## Hooks + +### useSchemaInitializer() + +用于获取 `SchemaInitializer` 上下文内容。 + +- 类型 + +```tsx | pure +export type InsertType = (s: ISchema) => void; + +const useSchemaInitializer: () => { + insert: InsertType; + options: SchemaInitializerOptions; + visible?: boolean; + setVisible?: (v: boolean) => void; +} +``` + +- 参数详解 + - `insert`:参数是 Schema 对象,用于插入 Schema + - `options`:获取 `new SchemaInitializer(options)` 时 options 配置 + - `visible`:popover 是否显示 + - `setVisible`:设置 popover 显示状态 + +- 示例 + +```tsx | pure +const schema = { + type: 'void', + 'x-component': 'Hello', +} +const Demo = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert(schema); + }; + return ; +} +``` + +### useSchemaInitializerRender() + +用于渲染 `SchemaInitializer`。 + +- 类型 + +```tsx | pure +function useSchemaInitializerRender(name: string, options?: SchemaInitializerOptions): { + exists: boolean; + render: (props?: SchemaInitializerOptions) => React.FunctionComponentElement; +} +``` + +- 参数详解 + +返回的 `render` 方法可以接收一个参数,用于覆盖 `new SchemaInitializer(options)` 的 `options` 配置。 + +- 示例 + +```tsx | pure +const Demo = () => { + const filedSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); + return
+
{ render() }
+
可以进行参数的二次覆盖:{ render({ style: { color: 'red' } }) }
+
+} +``` + + + +### useSchemaInitializerItem() + +用于获取配置项内容的,配置项是指的 `SchemaInitializer` 中的 `items` 中的一项。 + +- 类型 + +```tsx | pure +const useSchemaInitializerItem: () => T +``` + +- 示例 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'Item A', + foo: 'bar', + Component: Demo, + }, + ], +}); + +/** + * 通过 useSchemaInitializerItem() 获取到的是 + * { + * name: 'a', + * title: 'Item A', + * foo: 'bar', + * Component: Demo, + * } + */ +const Demo = () => { + const { title, foo } = useSchemaInitializerItem(); + return
{ title } - { foo }
+} +``` + + + +## 内置组件和类型 + +| type | Component | 效果 | +| ----------- | ------------------------------ | ----------------------------------------- | +| item | SchemaInitializerItem | 文本| +| itemGroup | SchemaInitializerItemGroup | 分组,同 antd `Menu` 组件的 `type: 'group'` | +| subMenu | SchemaInitializerSubMenu | 子菜单,同 antd `Menu` 组件的子菜单 | +| divider | SchemaInitializerDivider | 分割线,同 antd `Menu` 组件的 `type: 'divider'` | +| switch | SchemaInitializerSwitch | 开关 | +| actionModal | SchemaInitializerActionModal | 弹窗| + +以下每个示例都提供了 2 种[定义方式](/core/ui-schema/schema-initializer#两种定义方式component-和-type),一种是通过 `Component` 定义,另一种是通过 `type` 定义。 + +### `type: 'item'` & `SchemaInitializerItem` + +文本项。 + +```tsx | pure +interface SchemaInitializerItemProps { + style?: React.CSSProperties; + className?: string; + name?: string; + icon?: React.ReactNode; + title?: React.ReactNode; + items?: SchemaInitializerItemType[]; + onClick?: (args?: any) => any; +} +``` + +核心参数是 `title`、`icon`、`onClick`、`items`,其中 `onClick` 用于插入 Schema,`items` 用于渲染子列表项。 + + + +### `type: 'itemGroup'` & SchemaInitializerItemGroup + +分组。 + +```tsx | pure +interface SchemaInitializerItemGroupProps { + name: string; + title: string; + children?: SchemaInitializerOptions['items']; + divider?: boolean; +} +``` + +核心参数是 `title`、`children`,其中 `children` 用于渲染子列表项,`divider` 用于渲染分割线。 + + + +### `type: 'switch'` & SchemaInitializerSwitch + +Switch 切换按钮。 + +```tsx | pure +interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps { + checked?: boolean; + disabled?: boolean; +} +``` + +核心参数是 `checked`、`onClick`,其中 `onClick` 用于插入或者移除 Schema。 + + + +### `type: 'subMenu'` & SchemaInitializerSubMenu + +子菜单。 + + + +### `type: 'divider'` & SchemaInitializerDivider + +分割线。 + + + +## 渲染组件 + +### SchemaInitializerChildren + +用于自定义渲染多个列表项。 + +```tsx | pure + +const Demo = ({ children }) => { + // children: [{ name: 'a1', Component: ItemA1 }, { name: 'a2', type: 'item', title: 'ItemA2' }] + return { children } +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'test', + Component: Demo, + children: [ + { + name: 'a1', + Component: ItemA1, + }, + { + name: 'a2', + type: 'item', + title: 'ItemA2', + } + ] + } + ], +}); +``` + +### SchemaInitializerChild + +用于自定义渲染单个列表项。 + +```tsx | pure +const Demo = (props) => { + return +} +``` diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-settings-manager.md b/packages/core/client/docs/en-US/core/ui-schema/schema-settings-manager.md new file mode 100644 index 0000000000..e4f43a33d9 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-settings-manager.md @@ -0,0 +1,188 @@ +# SchemaSettingsManager + +## 实例方法 + +### schemaSettingsManager.add() + +添加 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + add(...schemaSettingList: SchemaSetting[]): void +} +``` + +- 示例 + +```tsx | pure +const mySchemaSettings = new SchemaSetting({ + name: 'MySchemaSettings', + title: 'Add block', + items: [ + { + name: 'demo', + type: 'item', + componentProps:{ + title: 'Demo' + } + } + ], +}); + +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.add(mySchemaSettings); + } +} +``` + +### schemaSettingsManager.get() + +获取一个 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + get(name: string): SchemaSetting | undefined +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.getAll() + +获取所有的 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + getAll(): Record> +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const list = this.app.schemaSettingsManager.getAll(); + } +} +``` + +### app.schemaSettingsManager.has() + +判断是否有存在某个 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + has(name: string): boolean +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasMySchemaSettings = this.app.schemaSettingsManager.has('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.remove() + +移除 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + remove(name: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.remove('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.addItem() + +添加 SchemaSettings 实例的 Item 项,其和直接 schemaInitializer.add() 方法的区别是,可以确保在实例存在时才会添加。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + addItem(schemaInitializerName: string, itemName: string, data: Omit): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再添加子项,需要确保已注册 + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + if (mySchemaSettings) { + mySchemaSettings.add('b', { type: 'item', componentProps:{ title: 'B' } }) + } + + // 方式2:通过 addItem,内部确保在 mySchemaSettings 注册时才会添加 + this.app.schemaSettingsManager.addItem('MySchemaSettings', 'b', { + type: 'item', + componentProps:{ title: 'B' } + }) + } +} +``` + +### schemaSettingsManager.removeItem() + +移除 实例的 Item 项,其和直接 schemaInitializer.remove() 方法的区别是,可以确保在实例存在时才会移除。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + removeItem(schemaInitializerName: string, itemName: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再删除子项,需要确保已注册 + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + if (mySchemaSettings) { + mySchemaSettings.remove('a') + } + + // 方式2:通过 addItem,内部确保在 mySchemaSettings 注册时才会移除 + this.app.schemaSettingsManager.remove('MySchemaSettings', 'a') + } +} +``` diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-settings.md b/packages/core/client/docs/en-US/core/ui-schema/schema-settings.md new file mode 100644 index 0000000000..466a131cb2 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-settings.md @@ -0,0 +1,467 @@ +# SchemaSettings + +## new SchemaSettings(options) + +创建一个 SchemaSettings 实例。 + +```tsx | pure +interface SchemaSettingsOptions { + name: string; + Component?: ComponentType; + componentProps?: T; + style?: React.CSSProperties; + + items: SchemaSettingsItemType[]; +} + +class SchemaSettings{ + constructor(options: SchemaSettingsOptions): SchemaSettings; + add(name: string, item: Omit): void + get(nestedName: string): SchemaSettingsItemType | undefined + remove(nestedName: string): void +} +``` + +### 详细解释 + +![](../static/VfPGbhWo9os0qTxcITkcHNfin4g.png) + +- name:唯一标识,必填 +- Component 相关 + + - Component:触发组件,默认是 `` 组件 + - componentProps: 组件属性 + - style:组件的样式 +- items:列表项配置 + +### 示例 + +#### 基础用法 + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + items: [ + { + name: 'demo1', // 唯一标识 + type: 'item', // 内置类型 + componentProps: { + title: 'DEMO1', + onClick() { + alert('DEMO1'); + }, + }, + }, + { + name: 'demo2', + Component: () => alert('DEMO2')} />, // 直接使用 Component 组件 + }, + ], +}); +``` + +#### 定制化 `Component` + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + Component: Button, // 自定义组件 + componentProps: { + type: 'primary', + children: '自定义按钮', + }, + // Component: (props) => , // 等同于上面效果 + items: [ + { + name: 'demo1', + type: 'item', + componentProps: { + title: 'DEMO', + }, + }, + ], +}); +``` + +## options.items 配置详解 + +```tsx | pure +interface SchemaSettingsItemCommon { + name: string; + sort?: number; + type?: string; + Component: string | ComponentType; + useVisible?: () => boolean; + children?: SchemaSettingsItemType[]; + useChildren?: () => SchemaSettingsItemType[]; + checkChildrenLength?: boolean; + componentProps?: Omit; + useComponentProps?: () => Omit; +} +``` + +### 两种定义方式:`Component` 和 `type` + + +- 通过 `Component` 定义 + +```tsx | pure + +const Demo = () => { + // 最终渲染 `SchemaSettingsItem` + return +} + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + } + ], +}); +``` + +- 通过 `type` 定义 + +NocoBase 内置了一些常用的 `type`,例如 `type: 'item'`,相当于 `Component: SchemaSettingsItem`。 + +更多内置类型,请参考:[内置组件和类型](/core/ui-schema/schema-settings#%E5%86%85%E7%BD%AE%E7%BB%84%E4%BB%B6%E5%92%8C%E7%B1%BB%E5%9E%8B) + +```tsx | pure +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'item', + componentProps: { + title: 'Demo', + }, + } + ], +}); +``` + + + +### `children` 和动态方式 `useChildren` + +对于某些组件而言是有子列表项的,例如 `type: 'itemGroup'`,那么我们使用 children 属性,同时考虑到某些场景下 children 是动态的,需要从 Hooks 里面获取,那么就可以通过 `useChildren` 来定义。 + + + +### 动态显示隐藏 `useVisible` + + + +### 组件属性 `componentProps` 和动态属性 `useComponentProps` + +对于一些通用组件,我们可以通过 `componentProps` 来定义组件属性,同时考虑到某些场景下组件属性是动态的,需要从 Hooks 里面获取,那么就可以通过 `useComponentProps` 来定义。 + +当然也可以不使用这两个属性,直接封装成一个组件,然后通过 `Component` 来定义。 + + + +## 实例方法 + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'item a' + }, + children: [ + { + name: 'a1', + title: 'item a1', + } + ], + }, + ], +}); +``` + +### schemaSettings.add() + +用于新增 Item。 + +- 类型 + +```tsx | pure +class SchemaSettings { + add(name: string, item: Omit): void +} +``` + +- 参数说明 + +第一个参数是 name,作为唯一标识,用于增删改查,并且 `name` 支持 `.` 用于分割层级。 + +- 示例 + +```tsx | pure +mySchemaSetting.add('b', { + type: 'item', + title: 'item b', +}) + +mySchemaSetting.add('a.a2', { + type: 'item', + title: 'item a2', +}) +``` + +### schemaSettings.get() + +- 类型 + +```tsx | pure +class SchemaSettings { + get(nestedName: string): SchemaSettingsItemType| undefined +} +``` + +- 示例 + +```tsx | pure +const itemA = mySchemaSetting.get('a') + +const itemA1 = mySchemaSetting.add('a.a1') +``` + +### schemaSettings.remove() + +- 类型 + +```tsx | pure +class SchemaSettings { + remove(nestedName: string): void +} +``` + +- 示例 + +```tsx | pure +mySchemaSetting.remove('a.a1') + +mySchemaSetting.remove('a') +``` + +## Hooks + +### useSchemaSettingsRender() + +用于渲染 SchemaInitializer。 + +- 类型 + +```tsx | pure +function useSchemaSettingsRender(name: string, options?: SchemaSettingsOptions): { + exists: boolean; + render: (options?: SchemaSettingsRenderOptions) => React.ReactElement; +} +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const filedSchema = useFieldSchema(); + const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], fieldSchema['x-settings-props']) + return
+
{ render() }
+
可以进行参数的二次覆盖:{ render({ style: { color: 'red' } }) }
+
+} +``` + + + +### useSchemaSettings() + +获取 schemaSetting 上下文数据。 + +上下文数据包含了 `schemaSetting` 实例化时的 `options` 以及调用 `useSchemaSettingsRender()` 时传入的 `options`。 + +- 类型 + +```tsx | pure +interface UseSchemaSettingsResult extends SchemaSettingsOptions { + dn?: Designable; + field?: GeneralField; + fieldSchema?: Schema; +} + +function useSchemaSettings(): UseSchemaSettingsResult; +``` + +- 示例 + +```tsx | pure +const { dn } = useSchemaSettings(); +``` + +### useSchemaSettingsItem() + +用于获取一个 item 的数据。 + +- 类型 + +```tsx | pure +export type SchemaSettingsItemType = { + name: string; + type?: string; + sort?: number; + Component?: string | ComponentType; + componentProps?: T; + useComponentProps?: () => T; + useVisible?: () => boolean; + children?: SchemaSettingsItemType[]; + [index]: any; +}; + +function useSchemaSettingsItem(): SchemaSettingsItemType; +``` + +- 示例 + +```tsx | pure +const { name } = useSchemaSettingsItem(); +``` + +## 内置组件和类型 + +| type | Component | 效果 | +| ----------- | ------------------------------ | ----------------------------------------- | +| item | SchemaSettingsItem | 文本 | +| itemGroup | SchemaSettingsItemGroup | 分组,同 Menu 组件的 `type: 'itemGroup'` | +| subMenu | SchemaSettingsSubMenu | 子菜单,同 Menu 组件的子菜单 | +| divider | SchemaSettingsDivider | 分割线,同 Menu 组件的 `type: 'divider'` | +| remove | SchemaSettingsRemove | 删除,用于删除一个区块 | +| select | SchemaSettingsSelectItem | 下拉选择 | +| cascader | SchemaSettingsCascaderItem | 级联选择 | +| switch | SchemaSettingsSwitchItem | 开关 | +| popup | SchemaSettingsPopupItem | 弹出层 | +| actionModal | SchemaSettingsActionModalItem | 操作弹窗 | +| modal | SchemaSettingsModalItem | 弹窗 | + +### SchemaSettingsItem + +文本,对应的 `type` 为 `item`。 + +```tsx | pure +interface SchemaSettingsItemProps extends Omit { + title: string; +} +``` + +核心参数为 `title` 和 `onClick`,可以在 `onClick` 中修改 schema。 + + + +### SchemaSettingsItemGroup + +分组,对应的 `type` 为 `itemGroup`。 + +核心参数是 `title`。 + + + +### SchemaSettingsSubMenu + +子菜单,对应的 `type` 为 `subMenu`。 + +核心参数是 `title`。 + + + +### SchemaSettingsDivider + +分割线,对应的 `type` 为 `divider`。 + + + +### SchemaSettingsRemove + +删除,对应的 `type` 为 `remove`。 + +```tsx | pure +interface SchemaSettingsRemoveProps { + confirm?: ModalFuncProps; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | ((s: ISchema) => boolean); +} +``` + +- `confirm`:删除前的确认弹窗 +- `removeParentsIfNoChildren`:如果删除后没有子节点了,是否删除父节点 +- `breakRemoveOn`:如果删除的节点满足条件,是否中断删除 + + + +### SchemaSettingsSelectItem + +选择器,对应的 `type` 为 `select`。 + + + +### SchemaSettingsCascaderItem + +级联选择,对应的 `type` 为 `cascader`。 + +### SchemaSettingsSwitchItem + +开关,对应的 `type` 为 `switch`。 + + + +### SchemaSettingsModalItem + +弹窗,对应的 `type` 为 `modal`。 + +```tsx | pure +export interface SchemaSettingsModalItemProps { + title: string; + onSubmit: (values: any) => void; + initialValues?: any; + schema?: ISchema | (() => ISchema); + modalTip?: string; + components?: any; + hidden?: boolean; + scope?: any; + effects?: any; + width?: string | number; + children?: ReactNode; + asyncGetInitialValues?: () => Promise; + eventKey?: string; + hide?: boolean; +} +``` + +我们可以通过 `schema` 参数来定义弹窗的表单,然后在 `onSubmit` 中获取表单的值,然后修改当前 schema 节点。 + + + +### SchemaSettingsActionModalItem + +操作弹窗,对应的 `type` 为 `actionModal`。 + +其和 `modal` 的区别是,`SchemaSettingsModalItem` 弹窗会丢失上下文,而 `SchemaSettingsActionModalItem` 会保留上下文,简单场景下可以使用 `SchemaSettingsModalItem`,复杂场景下可以使用 `SchemaSettingsActionModalItem`。 + +```tsx | pure +export interface SchemaSettingsActionModalItemProps extends SchemaSettingsModalItemProps, Omit { + uid?: string; + initialSchema?: ISchema; + schema?: ISchema; + beforeOpen?: () => void; + maskClosable?: boolean; +} +``` + + diff --git a/packages/core/client/docs/en-US/core/ui-schema/schema-toolbar.md b/packages/core/client/docs/en-US/core/ui-schema/schema-toolbar.md new file mode 100644 index 0000000000..dfd809bba0 --- /dev/null +++ b/packages/core/client/docs/en-US/core/ui-schema/schema-toolbar.md @@ -0,0 +1,427 @@ +# SchemaToolbar + +![SchemaToolbar](../static/designer.png) + +## 组件 + +### SchemaToolbar 组件 + +此为默认的 Toolbar 组件,其内部会自动根据 Schema 的 `x-settings`、`x-initializer` 渲染 `SchemaSettings`、`SchemaInitializer` 和 `Drag` 组件。 + +Toolbar 具体的渲染规则为:当有 `x-toolbar` 会渲染对应的组件;当无 `x-toolbar` 但是有 `x-settings`、`x-initializer` 会渲染默认的 `SchemaToolbar` 组件。 + +- 类型 + +```tsx | pure +interface SchemaToolbarProps { + title?: string; + draggable?: boolean; + initializer?: string | false; + settings?: string | false; + /** + * @default true + */ + showBorder?: boolean; + showBackground?: boolean; +} +``` + +- 详细解释 + - `title`:左上角的标题 + - `draggable`:是否可以拖拽,默认为 `true` + - `initializer`:`SchemaInitializer` 的默认值,当 schema 里没有 `x-initializer` 时,会使用此值;当为 `false` 时,不会渲染 `SchemaInitializer` + - `settings`:`SchemaSettings` 的默认值,当 schema 里没有 `x-settings` 时,会使用此值;当为 `false` 时,不会渲染 `SchemaSettings` + - `showBorder`:边框是否变为橘色 + - `showBackground`:背景是否变为橘色 + +- 示例 + +未指定 `x-toolbar` 时会渲染默认的 `SchemaToolbar` 这个组件。 + +```tsx +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 仅指定了 `x-settings` 但是没有 `x-toolbar`,会使用默认的 `SchemaToolbar` 组件 + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` + +自定义 Toolbar 组件。 + + +```tsx +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const MyToolbar = () => { + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello, MyToolbar }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` + +## Hooks + +### useSchemaToolbarRender() + +用于渲染 `SchemaToolbar`。 + +- 类型 + +```tsx | pure +const useSchemaToolbarRender: (fieldSchema: ISchema) => { + render(props?: SchemaToolbarProps): React.JSX.Element; + exists: boolean; +} +``` + +- 详细解释 + +前面示例中 `'x-decorator': 'CardItem'` 中组件 `CardItem` 里面就调用了 `useSchemaToolbarRender()` 进行渲染。内置的组件还有:`BlockItem`、`CardItem`、`Action`、`FormItem`。 + +`render()` 支持二次覆盖组件属性。 + +- 示例 + +```tsx | pure +const MyDecorator = () => { + const filedSchema = useFieldSchema(); + const { render } = useSchemaToolbarRender(filedSchema); // 从 Schema 中读取 Toolbar 组件 + + return { render() } +} +``` + + +```tsx +import { Card } from 'antd'; +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaToolbarRender +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const MyToolbar = (props) => { + return +} + +// 自定义包装器 +const MyDecorator = ({children}) => { + const filedSchema = useFieldSchema(); + // 使用 `useSchemaToolbarRender()` 获取并渲染内容 + const { render } = useSchemaToolbarRender(filedSchema); + return { render({ draggable: false }) }{children} +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'MyDecorator', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'MyDecorator', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello, MyToolbar, MyDecorator }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` diff --git a/packages/core/client/docs/en-US/core/utils.md b/packages/core/client/docs/en-US/core/utils.md new file mode 100644 index 0000000000..711c8ad0a8 --- /dev/null +++ b/packages/core/client/docs/en-US/core/utils.md @@ -0,0 +1,7 @@ +# Utils + +## Function + +### tval + +用于输出多语言的字符串。 diff --git a/packages/core/client/docs/en-US/index.md b/packages/core/client/docs/en-US/index.md new file mode 100644 index 0000000000..fe2d08520b --- /dev/null +++ b/packages/core/client/docs/en-US/index.md @@ -0,0 +1,3 @@ +# Index + + diff --git a/packages/core/client/docs/en-US/ui-schema/actions/add-new.md b/packages/core/client/docs/en-US/ui-schema/actions/add-new.md new file mode 100644 index 0000000000..308dab604c --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/add-new.md @@ -0,0 +1,82 @@ +# Add new + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-action": "create", + "x-acl-action": "create", + "title": "{{t('Add new')}}", + "x-designer": "Action.Designer", + "x-component": "Action", + "x-decorator": "ACLActionProvider", + "x-component-props": { + "openMode": "drawer", + "type": "primary", + "component": "CreateRecordAction", + "icon": "PlusOutlined" + }, + "x-align": "right", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Add record\") }}", + "x-component": "Action.Container", + "x-component-props": { + "className": "nb-action-popup" + }, + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializersForCreateFormBlock", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Add new\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "CreateFormBlockInitializers", + "x-uid": "psh2rj6pkab", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "3i7grk0aytf", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "75s24n1nlkh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "wg1f4xyx7vp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "2hwye7mt2th", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/add-record.md b/packages/core/client/docs/en-US/ui-schema/actions/add-record.md new file mode 100644 index 0000000000..7dbce5eda0 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/add-record.md @@ -0,0 +1 @@ +# Add record(任意表) \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/bulk-edit.md b/packages/core/client/docs/en-US/ui-schema/actions/bulk-edit.md new file mode 100644 index 0000000000..b602f30ff3 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/bulk-edit.md @@ -0,0 +1 @@ +# 批量编辑 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/bulk-update.md b/packages/core/client/docs/en-US/ui-schema/actions/bulk-update.md new file mode 100644 index 0000000000..9ac6d1890a --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/bulk-update.md @@ -0,0 +1 @@ +# 批量更新 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/custom-request.md b/packages/core/client/docs/en-US/ui-schema/actions/custom-request.md new file mode 100644 index 0000000000..b69407f624 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/custom-request.md @@ -0,0 +1,88 @@ +# 自定义请求 + + +x-component 不统一,x-action 也不统一,有三种风格的 schema + + +## UI Schema + +### 表单的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "CustomRequestAction", + "x-action": "customize:form:request", + "x-designer": "CustomRequestAction.Designer", + "x-decorator": "CustomRequestAction.Decorator", + "x-action-settings": { + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "type": "void", + "x-uid": "d1rfalvivfo", + "x-async": false, + "x-index": 2 +} +``` + +### 表格列操作的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "Action.Link", + "x-action": "customize:table:request", + "x-designer": "Action.Designer", + "x-action-settings": { + "requestSettings": {}, + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "x-component-props": { + "useProps": "{{ useCustomizeRequestActionProps }}" + }, + "x-designer-props": { + "linkageAction": true + }, + "type": "void", + "x-uid": "edn0y4fq7xb", + "x-async": false, + "x-index": 1 +} +``` + +### 详情的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "CustomRequestAction", + "x-action": "customize:form:request", + "x-designer": "CustomRequestAction.Designer", + "x-decorator": "CustomRequestAction.Decorator", + "x-action-settings": { + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "type": "void", + "x-uid": "glc5zkr0ke3", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/delete.md b/packages/core/client/docs/en-US/ui-schema/actions/delete.md new file mode 100644 index 0000000000..d4f3659f0c --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/delete.md @@ -0,0 +1 @@ +# 删除 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/duplicate.md b/packages/core/client/docs/en-US/ui-schema/actions/duplicate.md new file mode 100644 index 0000000000..5fc15c69d2 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/duplicate.md @@ -0,0 +1 @@ +# 复制 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/edit.md b/packages/core/client/docs/en-US/ui-schema/actions/edit.md new file mode 100644 index 0000000000..3092287aa7 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/edit.md @@ -0,0 +1 @@ +# 编辑 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/export.md b/packages/core/client/docs/en-US/ui-schema/actions/export.md new file mode 100644 index 0000000000..9e6bd4bd6b --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/export.md @@ -0,0 +1 @@ +# 导出 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/filter.md b/packages/core/client/docs/en-US/ui-schema/actions/filter.md new file mode 100644 index 0000000000..653fc6d266 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/filter.md @@ -0,0 +1 @@ +# 筛选 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/import.md b/packages/core/client/docs/en-US/ui-schema/actions/import.md new file mode 100644 index 0000000000..a371bffd35 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/import.md @@ -0,0 +1 @@ +# 导入 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/index.md b/packages/core/client/docs/en-US/ui-schema/actions/index.md new file mode 100644 index 0000000000..e177708c44 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/index.md @@ -0,0 +1,51 @@ +# 概览 + +## UI Schema 协议 + +- `x-action` +- `x-align` +- `x-acl-action` +- `x-acl-action-props` +- `x-action-settings` + +## 常规操作 + +```json +{ + "type": "void", + "title": "{{ t(\"Submit\") }}", + "x-component": "Action", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, +} +``` + +## 弹窗操作 + +```json +{ + "type": "void", + "title": "{{t('Open drawer')}}", + "x-component": "Action", + "x-component-props": { + "openMode": "drawer", + "type": "primary", + "icon": "PlusOutlined" + }, + "x-align": "right", + "properties": { + "drawer": { + "type": "void", + "x-component": "Action.Container", + "x-component-props": {}, + "properties": { + }, + } + } +} +``` + +更多示例查看 [Action](/apis/action) \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/print.md b/packages/core/client/docs/en-US/ui-schema/actions/print.md new file mode 100644 index 0000000000..2823a7a182 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/print.md @@ -0,0 +1 @@ +# 打印 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/refresh.md b/packages/core/client/docs/en-US/ui-schema/actions/refresh.md new file mode 100644 index 0000000000..654d97cca9 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/refresh.md @@ -0,0 +1 @@ +# 刷新 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/save-record.md b/packages/core/client/docs/en-US/ui-schema/actions/save-record.md new file mode 100644 index 0000000000..83d1a5a372 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/save-record.md @@ -0,0 +1 @@ +# 保存数据 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/submit-to-workflow.md b/packages/core/client/docs/en-US/ui-schema/actions/submit-to-workflow.md new file mode 100644 index 0000000000..97bfe3b191 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/submit-to-workflow.md @@ -0,0 +1 @@ +# 提交至工作流 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/submit.md b/packages/core/client/docs/en-US/ui-schema/actions/submit.md new file mode 100644 index 0000000000..0634eb49d2 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/submit.md @@ -0,0 +1,28 @@ +# 提交 + +## UI Schema + +### 新建数据的提交 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Submit\") }}", + "x-action": "submit", + "x-component": "Action", + "x-designer": "Action.Designer", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, + "x-action-settings": { + "triggerWorkflows": [] + }, + "type": "void", + "x-uid": "aaxbm1i5xxd", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/update-record.md b/packages/core/client/docs/en-US/ui-schema/actions/update-record.md new file mode 100644 index 0000000000..aa3ff7ffb3 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/update-record.md @@ -0,0 +1 @@ +# 更新数据 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/actions/view.md b/packages/core/client/docs/en-US/ui-schema/actions/view.md new file mode 100644 index 0000000000..f5fd55d6b8 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/actions/view.md @@ -0,0 +1 @@ +# 查看 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/calendar.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/calendar.md new file mode 100644 index 0000000000..13e6c108e6 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/calendar.md @@ -0,0 +1,282 @@ +# Calendar + +## x-initializer + +- CalendarActionInitializers +- TabPaneInitializers +- RecordBlockInitializers + +## x-designer + +- CalendarV2.Designer + +## x-settings + +- CalendarSettings + +## UI Schema + +### 日历区块 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "CalendarV2.Designer", + "x-component": "CardItem", + "x-decorator": "CalendarBlockProvider", + "x-acl-action": "users:list", + "x-decorator-props": { + "action": "list", + "params": { + "paginate": false + }, + "resource": "users", + "collection": "users", + "fieldNames": { + "id": "id", + "end": ["f_nxqfedh5fd5"], + "start": ["f_nxqfedh5fd5"], + "title": "nickname" + } + }, + "_isJSONSchemaObject": true, + "properties": { + "puytl7p5x8o": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2", + "x-component-props": { + "useProps": "{{ useCalendarBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "toolBar": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2.ActionBar", + "x-initializer": "CalendarActionInitializers", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "_isJSONSchemaObject": true, + "x-uid": "494h1j6bxyg", + "x-async": false, + "x-index": 1 + }, + "event": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2.Event", + "_isJSONSchemaObject": true, + "properties": { + "drawer": { + "type": "void", + "title": "{{ t(\"View record\") }}", + "version": "2.0", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "_isJSONSchemaObject": true, + "properties": { + "tabs": { + "type": "void", + "version": "2.0", + "x-component": "Tabs", + "x-initializer": "TabPaneInitializers", + "x-component-props": {}, + "_isJSONSchemaObject": true, + "x-initializer-props": { + "gridInitializer": "RecordBlockInitializers" + }, + "properties": { + "tab1": { + "type": "void", + "title": "{{t(\"Details\")}}", + "version": "2.0", + "x-designer": "Tabs.Designer", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "_isJSONSchemaObject": true, + "properties": { + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "_isJSONSchemaObject": true, + "x-initializer-props": { + "actionInitializers": "CalendarFormActionInitializers" + }, + "x-uid": "yty9wg20qq0", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "hcsgenoc3hb", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "c5cpptwxw7s", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "c5iarcnpefc", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "080x8qlpn6s", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "pcche4s596p", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "gk2jk3gpo9f", + "x-async": false, + "x-index": 1 +} +``` + +### 关系的日历区块 + +```json +{ + "x-uid": "8q0hsbdr9fc", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:list", + "x-decorator": "CalendarBlockProvider", + "x-decorator-props": { + "collection": "b", + "resource": "a.o2m", + "action": "list", + "fieldNames": { + "id": "id", + "start": "createdAt", + "title": "f_ex4581dfc0t" + }, + "params": { + "paginate": false + }, + "association": "a.o2m" + }, + "x-designer": "CalendarV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系的日历区块" + }, + "properties": { + "v3za6qejuz7": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2", + "x-component-props": { + "useProps": "{{ useCalendarBlockProps }}" + }, + "properties": { + "toolBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2.ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-initializer": "CalendarActionInitializers", + "x-uid": "u9ntffsdsrh", + "x-async": false, + "x-index": 1 + }, + "event": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2.Event", + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "x-initializer-props": { + "gridInitializer": "RecordBlockInitializers" + }, + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer-props": { + "actionInitializers": "CalendarFormActionInitializers" + }, + "x-initializer": "RecordBlockInitializers", + "x-uid": "2swhiw4lee5", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "z6odjfbwnht", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mxr7ikhfc9y", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mfgucnj7ev8", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "05l5a2h42pn", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "v2jazzvqa85", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/charts.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/charts.md new file mode 100644 index 0000000000..77dbdbdbb9 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/charts.md @@ -0,0 +1,143 @@ +# Charts + +## UI Schema + +### 图表区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CardItem", + "x-component-props": { + "name": "charts" + }, + "x-designer": "ChartV2BlockDesigner", + "properties": { + "u8841bjm65y": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-decorator": "ChartV2Block", + "x-initializer": "ChartInitializers", + "x-uid": "j4hzcpzcc3g", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ldc2aokaop2", + "x-async": false, + "x-index": 1 +} +``` + +### 内嵌区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CardItem", + "x-component-props": { + "name": "charts" + }, + "x-designer": "ChartV2BlockDesigner", + "properties": { + "g3jq3vkx9fv": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-decorator": "ChartV2Block", + "x-initializer": "ChartInitializers", + "properties": { + "ue7lfigjgm1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Row", + "properties": { + "43ztg8gu03v": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Col", + "properties": { + "n47fsb4i2lm": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "ChartRendererProvider", + "x-decorator-props": { + "query": { + "measures": [ + { + "field": ["id"] + } + ], + "dimensions": [], + "filter": { + "$and": [] + }, + "orders": [], + "cache": {} + }, + "config": { + "chartType": "Built-in.line", + "general": { + "xField": "id", + "yField": "id", + "smooth": false, + "isStack": false + } + }, + "collection": "a", + "mode": "builder" + }, + "x-acl-action": "a:list", + "x-designer": "ChartRenderer.Designer", + "x-component": "CardItem", + "x-component-props": { + "size": "small" + }, + "x-initializer": "ChartInitializers", + "properties": { + "y25sgh5pukl": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "ChartRenderer", + "x-component-props": {}, + "x-uid": "ma0s07k753t", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "k6naqcx59ow", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "o8ktlccovml", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mvn9ai9rhx8", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mcbyby93j6n", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "sn0lczwtkgm", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/details.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/details.md new file mode 100644 index 0000000000..31f765bacb --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/details.md @@ -0,0 +1,169 @@ +# Details + +## UI Schema + +### 详情区块(带分页) + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "DetailsDesigner", + "x-component": "CardItem", + "x-decorator": "DetailsBlockProvider", + "x-acl-action": "users:view", + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 1 + }, + "rowKey": "id", + "resource": "users", + "collection": "users", + "readPretty": true + }, + "_isJSONSchemaObject": true, + "properties": { + "mjad1dcywip": { + "type": "void", + "version": "2.0", + "x-component": "Details", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useDetailsBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "8avswx6qgyp": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "DetailsActionInitializers", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "_isJSONSchemaObject": true, + "x-uid": "01w17yleng8", + "x-async": false, + "x-index": 1 + }, + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "_isJSONSchemaObject": true, + "x-uid": "bgu0ix4lfmc", + "x-async": false, + "x-index": 2 + }, + "pagination": { + "type": "void", + "version": "2.0", + "x-component": "Pagination", + "x-component-props": { + "useProps": "{{ useDetailsPaginationProps }}" + }, + "_isJSONSchemaObject": true, + "x-uid": "6riuxghrz30", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "3apkfu8ngrp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "yeodhismy2w", + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系区块 + +```json +{ + "x-uid": "2gm2iw937q5", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "DetailsBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 1 + }, + "rowKey": "id" + }, + "x-designer": "DetailsDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "关系区块" + }, + "properties": { + "paj8b2noc1g": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Details", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useDetailsBlockProps }}" + }, + "properties": { + "3jisjip7503": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "DetailsActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "cbkfxafywtx", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "d1ls1w6gpzm", + "x-async": false, + "x-index": 2 + }, + "pagination": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Pagination", + "x-component-props": { + "useProps": "{{ useDetailsPaginationProps }}" + }, + "x-uid": "u0rzv8370za", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "1hfu8jbzzqw", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/form-read-pretty.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/form-read-pretty.md new file mode 100644 index 0000000000..676df30347 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/form-read-pretty.md @@ -0,0 +1,286 @@ +# Form (Read pretty) + +## UI Schema + +### 单数据详情 + +```json +{ + "x-uid": "dt4q817fw81", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a", + "collection": "a", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "Form read pretty" + }, + "properties": { + "30qrxij609o": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "fznn1ygt50o", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "t3o4t8fi7l7", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "6dp2qqoc4s0", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系数据详情 + +```json +{ + "x-uid": "jf470wrzdaf", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2oBelongsTo:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2oBelongsTo", + "collection": "b", + "association": "a.o2oBelongsTo", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "对一关系详情" + }, + "properties": { + "uauk2wbj3iq": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "r3pcykksp8l", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "vs000etludp", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "rt228uhudcf", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 表格详情 + +```json +{ + "x-uid": "lxcjjkkor1m", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "表格详情" + }, + "properties": { + "7pe17tcgtye": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "wek8nc795ja", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "5y5u8qm6yvz", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "5bpcm982n51", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 查看关系数据 + +```json +{ + "x-uid": "5ymsrq1fv37", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "查看关系数据" + }, + "properties": { + "vehcfr0fsh2": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "zpuz629u2dg", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "b8u8e2k5inf", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "2rz0u3j14px", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/form.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/form.md new file mode 100644 index 0000000000..442457ba05 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/form.md @@ -0,0 +1,418 @@ +# Form + +## UI Schema + +### 添加表单 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a", + "collection": "a" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": {}, + "properties": { + "23pj9m7ot5a": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "0ctuwnatyzd", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "properties": { + "j7ta88et3mv": { + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Submit\") }}", + "x-action": "submit", + "x-component": "Action", + "x-designer": "Action.Designer", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, + "x-action-settings": { + "triggerWorkflows": [] + }, + "type": "void", + "x-uid": "ua858zswy37", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "o0riryr0ob0", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "1ng4hies4yu", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ebiyvziafda", + "x-async": false, + "x-index": 1 +} +``` + +### 编辑表单 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": false + }, + "x-acl-action": "a:update", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": "get", + "resource": "a", + "collection": "a" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": {}, + "properties": { + "w533dipin7p": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "otncpmalhjr", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "UpdateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "qjjmm1qkapx", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "n12zo6qd72g", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "zafyvmng9m5", + "x-async": false, + "x-index": 1 +} +``` + +### 关系新增表单 + +```json +{ + "x-uid": "il5q341k4vg", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a.o2m:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": null, + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系新增表单" + }, + "properties": { + "3357yvwe4je": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "88xhnoml321", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "dhs8o8vpl1h", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "h6fhh9ox9ay", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系新增(关系表格里的 Add new) + +useSourceIdFromParentRecord 和 useParamsFromRecord 参数缺失 + +```json +{ + "x-uid": "6tknohm4ubr", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a.o2m:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系新增表单-Table" + }, + "properties": { + "o96izt6ez7m": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "properties": { + "pk9l1i26yq6": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Row", + "properties": { + "32wz2lr9cry": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Col", + "properties": { + "id": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "string", + "x-designer": "FormItem.Designer", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "b.id", + "x-component-props": {}, + "x-read-pretty": true, + "x-uid": "jb09nj4kbfm", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "rtx3b0uom75", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5gvd7ks6m0z", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "fcw1y2xk6km", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "msgopb1meoq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "g0mg6fhho4x", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系编辑 + +```json +{ + "x-uid": "kzg1tz339ro", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": false + }, + "x-acl-action": "a.o2m:update", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": "get", + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系编辑表单" + }, + "properties": { + "3jqysycq8m0": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "sy3sjswogh4", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "UpdateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "xarcyjpypsy", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "lepez9684rm", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/gantt.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/gantt.md new file mode 100644 index 0000000000..4b7ad1269c --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/gantt.md @@ -0,0 +1,185 @@ +# Gantt + +## UI Schema + +### 甘特图区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "users:list", + "x-decorator": "GanttBlockProvider", + "x-decorator-props": { + "collection": "users", + "resource": "users", + "action": "list", + "fieldNames": { + "id": "id", + "start": "createdAt", + "range": "day", + "title": "nickname", + "end": "f_46b1u4dp820" + }, + "params": { + "paginate": false + } + }, + "x-designer": "Gantt.Designer", + "x-component": "CardItem", + "properties": { + "rx21gtpcj3w": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Gantt", + "x-component-props": { + "useProps": "{{ useGanttBlockProps }}" + }, + "properties": { + "toolBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-initializer": "GanttActionInitializers", + "x-uid": "m0uru68ugw9", + "x-async": false, + "x-index": 1 + }, + "table": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-decorator": "div", + "x-decorator-props": { + "style": { + "float": "left", + "maxWidth": "35%" + } + }, + "x-initializer": "TableColumnInitializers", + "x-component": "TableV2", + "x-component-props": { + "rowKey": "id", + "rowSelection": { + "type": "checkbox" + }, + "useProps": "{{ useTableBlockProps }}", + "pagination": false + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Actions\") }}", + "x-action-column": "actions", + "x-decorator": "TableV2.Column.ActionBar", + "x-component": "TableV2.Column", + "x-designer": "TableV2.ActionColumnDesigner", + "x-initializer": "TableActionColumnInitializers", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "DndContext", + "x-component": "Space", + "x-component-props": { + "split": "|" + }, + "x-uid": "z2vnizu7bds", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "8gbabo6kdr6", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5c8g2x6rvro", + "x-async": false, + "x-index": 2 + }, + "detail": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Gantt.Event", + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "jr5h3asz6gp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "8uwfpgyok9x", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "bjm4sfwxmrx", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "7cmwpc0rrwj", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nffb7qbs96o", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "hoi2j5lhlho", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "e0rdive4xpa", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/grid-card.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/grid-card.md new file mode 100644 index 0000000000..6620d607bc --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/grid-card.md @@ -0,0 +1,219 @@ +# GridCard + +## UI Schema + +### 栅格卡片 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "GridCard.Designer", + "x-component": "BlockItem", + "x-decorator": "GridCard.Decorator", + "x-acl-action": "users:view", + "x-component-props": { + "useProps": "{{ useGridCardBlockItemProps }}" + }, + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 12 + }, + "rowKey": "id", + "resource": "users", + "collection": "users", + "readPretty": true, + "runWhenParamsChanged": true + }, + "_isJSONSchemaObject": true, + "properties": { + "actionBar": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "GridCardActionInitializers", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "_isJSONSchemaObject": true, + "x-uid": "prx9wnlzqb1", + "x-async": false, + "x-index": 1 + }, + "list": { + "type": "array", + "version": "2.0", + "x-component": "GridCard", + "x-component-props": { + "useProps": "{{ useGridCardBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "item": { + "type": "object", + "version": "2.0", + "x-component": "GridCard.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useGridCardItemProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "_isJSONSchemaObject": true, + "x-initializer-props": { + "useProps": "{{ useGridCardItemInitializerProps }}" + }, + "x-uid": "pq895sv6136", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "type": "void", + "version": "2.0", + "x-align": "left", + "x-component": "ActionBar", + "x-initializer": "GridCardItemActionInitializers", + "x-component-props": { + "layout": "one-column", + "useProps": "{{ useGridCardActionBarProps }}" + }, + "_isJSONSchemaObject": true, + "x-uid": "w2c3qcp4nme", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "1ct8yn1jnzn", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "pvnkvtc6tte", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "phodsqwsifq", + "x-async": false, + "x-index": 1 +} +``` + +### 关系数据的栅格卡片 + +```json +{ + "x-uid": "yr79zvl98lc", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "GridCard.Decorator", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 12 + }, + "runWhenParamsChanged": true, + "columnCount": { + "xs": 1, + "md": 12, + "lg": 12, + "xxl": 12 + } + }, + "x-component": "BlockItem", + "x-component-props": { + "useProps": "{{ useGridCardBlockItemProps }}" + }, + "x-designer": "GridCard.Designer", + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "GridCardActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "u36l44beq6a", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "GridCard", + "x-component-props": { + "useProps": "{{ useGridCardBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "GridCard.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useGridCardItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useGridCardItemInitializerProps }}" + }, + "x-uid": "aqxlonv3bpk", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "GridCardItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useGridCardActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "zxhqo81kdf5", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "ed56owlyz1p", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nfpuvrkwkk7", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/index.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/index.md new file mode 100644 index 0000000000..a88afb3b92 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/index.md @@ -0,0 +1,255 @@ +# 数据区块概述 + +数据可能是某个表(collection)的数据,也可能是某个关系(association)的数据 + +- collection,如 a 表 +- association,如 a.b 关系,关系表为 b + +collection 示例 + +```js +{ + "collection": "a" +} +``` + +association 示例 + +```js +{ + "collection": "b", + "association": "a.b" +} +``` + +数据的操作 + +```bash +: +.: +``` + +实际的请求参数 + +``` + /api/: + /api/:/ + /api///: + /api///:/ +``` + +- collection、association、action 是配置里存好的 +- sourceId、filterByTk 由上下文提供 + +## RecordProvider + +Record + +```ts +class Record { + protected current = {}; + protected parent?: Record; + public isNew = false; + constructor(options = {}) { + const { current, parent, isNew } = options; + this.current = current || {}; + this.parent = parent; + this.isNew = isNew; + } +} +``` + +a:list(**list 外层不套空 RecordPicker**) + +```jsx | pure +{list.map((item) => { + const record = new Record({ current: item }); + return +})} +``` + +a:get(view、edit) + +```jsx | pure +const record = new Record({ current: item }); + +``` + +a:create + +```jsx | pure +const record = new Record({ current, isNew: true }); + +``` + +a.b:list + +```jsx | pure + + {list.map((item) => { + const recordB = new Record({ + current: item, + }); + return + })} + +``` + +a.b:get + +```jsx | pure +const recordA = new Record({ + current: itemA, +}); +const recordB = new Record({ + current: itemB, + parent: recordA, +}); +// 或者 +recordB.setParent(recordA); + + + + +``` + +a.b:create + +```jsx | pure +const recordA = new Record({ + current: itemA, +}); +const recordB = new Record({ + isNew: true, + parent: recordA, +}); + + + +``` + +## 区块 + +以下重要组件说明 + +- `DataBlockProvider` 数据区块的总称 +- `BlockProvider` 区块的各种参数设置 +- `CollectionProvider` collection 信息 +- `AssociationProvider` association 信息 +- `UseRequestProvider` useRequest 的 result + +没有当前记录的区块,如表格 + +```tsx | pure + + + + + + +
+ + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + {props.children} + + + +``` + +有当前记录的区块,如表单 + +```tsx | pure + +
+ +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + {props.children} + + + + +``` + +有当前父级记录的区块,如对多关系的表格区块,区块本身不套 RecordPicker + +```tsx | pure + + + + + + + +
+ + + + + + + + +
+ + + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + {props.children} + + + + +``` + +有父级记录也有当前记录的区块,如关系的表单 + +```tsx | pure + + + + + + + + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + + {props.children} + + + + + +``` diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/kanban.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/kanban.md new file mode 100644 index 0000000000..b84b1b62a4 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/kanban.md @@ -0,0 +1,160 @@ +# Kanban + +## UI Schema + +### 看板区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "t_ehwqkw0ljw2:list", + "x-decorator": "KanbanBlockProvider", + "x-decorator-props": { + "collection": "t_ehwqkw0ljw2", + "resource": "t_ehwqkw0ljw2", + "action": "list", + "groupField": "f_5h4umwf59lc", + "params": { + "sort": ["f_5h4umwf59lc_sort"], + "paginate": false + } + }, + "x-designer": "Kanban.Designer", + "x-component": "CardItem", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "KanbanActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "4vh5t2iohwp", + "x-async": false, + "x-index": 1 + }, + "iwnc45ou960": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "Kanban", + "x-component-props": { + "useProps": "{{ useKanbanBlockProps }}" + }, + "properties": { + "card": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-read-pretty": true, + "x-label-disabled": true, + "x-decorator": "BlockItem", + "x-component": "Kanban.Card", + "x-component-props": { + "openMode": "drawer" + }, + "x-designer": "Kanban.Card.Designer", + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-component-props": { + "dndContext": false + }, + "x-uid": "3v010y09684", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "dk9a5cb9d95", + "x-async": false, + "x-index": 1 + }, + "cardViewer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"View\") }}", + "x-designer": "Action.Designer", + "x-component": "Kanban.CardViewer", + "x-action": "view", + "x-component-props": { + "openMode": "drawer" + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"View record\") }}", + "x-component": "Action.Container", + "x-component-props": { + "className": "nb-action-popup" + }, + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "1lhiyyydpye", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "gisuj0zldz1", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ohly2w4gnwp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "29hjrxcfczh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "0k56h1q80xo", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "uny2d4n5ry4", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "3gga5lop4t6", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/list.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/list.md new file mode 100644 index 0000000000..4e690cb7d9 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/list.md @@ -0,0 +1,213 @@ +# List + +## UI Schema + +### 列表区块 + +```json +{ + "x-uid": "h9mzrnxokk3", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a:view", + "x-decorator": "List.Decorator", + "x-decorator-props": { + "resource": "a", + "collection": "a", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 10 + }, + "runWhenParamsChanged": true, + "rowKey": "id" + }, + "x-component": "CardItem", + "x-designer": "List.Designer", + "x-component-props": { + "title": "List 区块" + }, + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ListActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "chjgpgd45qz", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "List", + "x-component-props": { + "props": "{{ useListBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "List.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useListItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useListItemInitializerProps }}" + }, + "x-uid": "u45nwmwlone", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "ListItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useListActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "9erso7nhotq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "qkuzlun8frh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "1mfaa48qy5j", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系的列表区块 + +```json +{ + "x-uid": "6tzpnkz60rx", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "List.Decorator", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 10 + }, + "runWhenParamsChanged": true + }, + "x-component": "CardItem", + "x-designer": "List.Designer", + "x-component-props": { + "title": "对多关系的列表区块" + }, + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ListActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "i05xr7nfs26", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "List", + "x-component-props": { + "props": "{{ useListBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "List.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useListItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useListItemInitializerProps }}" + }, + "x-uid": "krerg70a28e", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "ListItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useListActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "9745q36ltfv", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "yxc0lnax9gk", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "6mlzb7p6dvn", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/map.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/map.md new file mode 100644 index 0000000000..0413da64be --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/map.md @@ -0,0 +1,115 @@ +# Map + +## UI Schema + +### 地图区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "t_ddo5bd0swdg:list", + "x-decorator": "MapBlockProvider", + "x-decorator-props": { + "collection": "t_ddo5bd0swdg", + "resource": "t_ddo5bd0swdg", + "action": "list", + "fieldNames": { + "field": ["f_tmhy5bbusr8"] + }, + "params": { + "paginate": false + } + }, + "x-designer": "MapBlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "MapActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 16 + } + }, + "x-uid": "ap0xzy32bms", + "x-async": false, + "x-index": 1 + }, + "1672zefmplu": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "MapBlock", + "x-component-props": { + "useProps": "{{ useMapBlockProps }}" + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "vce3u8hfdrf", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "waw7z3qw7nn", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "szyybarlapl", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "83m07ksqbal", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5cquhqq2161", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "p42awfpfnbf", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/data/table.md b/packages/core/client/docs/en-US/ui-schema/blocks/data/table.md new file mode 100644 index 0000000000..4c99e25545 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/data/table.md @@ -0,0 +1,193 @@ +# Table + +## UI Schema + +### 表格区块 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-decorator": "TableBlockProvider", + "x-acl-action": "users:list", + "x-filter-targets": [], + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 20 + }, + "rowKey": "id", + "dragSort": false, + "resource": "users", + "showIndex": true, + "collection": "users", + "disableTemplate": false + }, + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "TableActionInitializers", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "_isJSONSchemaObject": true, + "x-uid": "55tgsn1puqk", + "x-async": false, + "x-index": 1 + }, + "g22il5nkv67": { + "type": "array", + "version": "2.0", + "x-component": "TableV2", + "x-initializer": "TableColumnInitializers", + "x-component-props": { + "rowKey": "id", + "useProps": "{{ useTableBlockProps }}", + "rowSelection": { + "type": "checkbox" + } + }, + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "title": "{{ t(\"Actions\") }}", + "version": "2.0", + "x-designer": "TableV2.ActionColumnDesigner", + "x-component": "TableV2.Column", + "x-decorator": "TableV2.Column.ActionBar", + "x-initializer": "TableActionColumnInitializers", + "x-action-column": "actions", + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "version": "2.0", + "x-component": "Space", + "x-decorator": "DndContext", + "x-component-props": { + "split": "|" + }, + "_isJSONSchemaObject": true, + "x-uid": "qkcugf5l0h6", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "kbgbskxsj3j", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "6r8e3mhnuxg", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "b87bbc5x0cp", + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "TableBlockProvider", + "x-acl-action": "a.o2m:list", + "x-decorator-props": { + "collection": "b", + "resource": "a.o2m", + "action": "list", + "params": { + "pageSize": 20 + }, + "showIndex": true, + "dragSort": false, + "disableTemplate": false, + "association": "a.o2m" + }, + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "TableActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "0gwze38mzps", + "x-async": false, + "x-index": 1 + }, + "0wuk1cpy5zp": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-initializer": "TableColumnInitializers", + "x-component": "TableV2", + "x-component-props": { + "rowKey": "id", + "rowSelection": { + "type": "checkbox" + }, + "useProps": "{{ useTableBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Actions\") }}", + "x-action-column": "actions", + "x-decorator": "TableV2.Column.ActionBar", + "x-component": "TableV2.Column", + "x-designer": "TableV2.ActionColumnDesigner", + "x-initializer": "TableActionColumnInitializers", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "DndContext", + "x-component": "Space", + "x-component-props": { + "split": "|" + }, + "x-uid": "gq0nkc7rf0q", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "zx97354nzfq", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nss8uocacdq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "0j5kcrius5z", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/filter/collapse.md b/packages/core/client/docs/en-US/ui-schema/blocks/filter/collapse.md new file mode 100644 index 0000000000..0bb30600a9 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/filter/collapse.md @@ -0,0 +1,41 @@ +# Collapse + +## UI Schema + +### 折叠面板 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "AssociationFilter.Provider", + "x-decorator-props": { + "collection": "users", + "blockType": "filter", + "associationFilterStyle": { + "width": "100%" + }, + "name": "filter-collapse" + }, + "x-designer": "AssociationFilter.BlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "x8q0r7wp73e": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-action": "associateFilter", + "x-initializer": "AssociationFilter.FilterBlockInitializer", + "x-component": "AssociationFilter", + "x-uid": "j11suxuizte", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "0g6pyonrj83", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/filter/form.md b/packages/core/client/docs/en-US/ui-schema/blocks/filter/form.md new file mode 100644 index 0000000000..f0145c51b7 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/filter/form.md @@ -0,0 +1,63 @@ +# Form + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "FilterFormBlockProvider", + "x-decorator-props": { + "resource": "users", + "collection": "users" + }, + "x-designer": "FormV2.FilterDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "x-filter-operators": {}, + "properties": { + "dwltvxybiir": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FilterFormItemInitializers", + "x-uid": "aggw48stzwc", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "FilterFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "float": "right" + } + }, + "x-uid": "7aogdo89a7s", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "wreuz6dr1pc", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "7d1o5bruekl", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/index.md b/packages/core/client/docs/en-US/ui-schema/blocks/index.md new file mode 100644 index 0000000000..07dd0c5c77 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/index.md @@ -0,0 +1 @@ +# Overview diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/others/iframe.md b/packages/core/client/docs/en-US/ui-schema/blocks/others/iframe.md new file mode 100644 index 0000000000..58a2bf0380 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/others/iframe.md @@ -0,0 +1,22 @@ + +# iframe + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "Iframe.Designer", + "x-component": "Iframe", + "x-decorator": "BlockItem", + "x-component-props": {}, + "x-decorator-props": { + "name": "iframe" + }, + "_isJSONSchemaObject": true, + "x-uid": "fmmlqe8rrei", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/others/markdown.md b/packages/core/client/docs/en-US/ui-schema/blocks/others/markdown.md new file mode 100644 index 0000000000..478fafd2ce --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/others/markdown.md @@ -0,0 +1,24 @@ +# Markdown + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "Markdown.Void.Designer", + "x-editable": false, + "x-component": "Markdown.Void", + "x-decorator": "CardItem", + "x-component-props": { + "content": "This is a demo text, **supports Markdown syntax**." + }, + "x-decorator-props": { + "name": "markdown" + }, + "_isJSONSchemaObject": true, + "x-uid": "w75e9d5vs1e", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/blocks/others/workflow-todo.md b/packages/core/client/docs/en-US/ui-schema/blocks/others/workflow-todo.md new file mode 100644 index 0000000000..9f477e5d78 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/blocks/others/workflow-todo.md @@ -0,0 +1,33 @@ +# Workflow todos + +## x-designer + +- TableBlockDesigner(不适合直接用表格区块的) + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-decorator": "WorkflowTodo.Decorator", + "x-decorator-props": {}, + "_isJSONSchemaObject": true, + "properties": { + "todos": { + "type": "void", + "version": "2.0", + "x-component": "WorkflowTodo", + "_isJSONSchemaObject": true, + "x-uid": "xq1qcdpq7rq", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "3z3ey8r6ik9", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/cascader-select.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/cascader-select.md new file mode 100644 index 0000000000..94547e50cb --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/cascader-select.md @@ -0,0 +1 @@ +# 级联选择器 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/file-manager.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/file-manager.md new file mode 100644 index 0000000000..5b02970f76 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/file-manager.md @@ -0,0 +1 @@ +# 文件管理器 diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/index.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/index.md new file mode 100644 index 0000000000..e9f57784b4 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/index.md @@ -0,0 +1,21 @@ +# 关系字段组件 + +### 标题 Read pretty + +### 标签 Read pretty + +### 子详情 Read pretty + +### 子表单 Editable + +### 子表单(弹窗) Editable + +### 下拉选择器 Editable + +### 数据选择器 Editable + +### 级联选择器 Editable + +### 文件管理器 Editable + +### 子表格 Editable Read pretty diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/record-picker.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/record-picker.md new file mode 100644 index 0000000000..ae439f012a --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/record-picker.md @@ -0,0 +1 @@ +# 数据选择器 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/select.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/select.md new file mode 100644 index 0000000000..9df4627f7c --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/select.md @@ -0,0 +1 @@ +# 下拉选择器 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-details.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-details.md new file mode 100644 index 0000000000..a509906e4f --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-details.md @@ -0,0 +1 @@ +# 子详情 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form-popover.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form-popover.md new file mode 100644 index 0000000000..acc10f4a72 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form-popover.md @@ -0,0 +1 @@ +# 子表单 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form.md new file mode 100644 index 0000000000..acc10f4a72 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-form.md @@ -0,0 +1 @@ +# 子表单 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-table.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-table.md new file mode 100644 index 0000000000..9556a6c5ce --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/sub-table.md @@ -0,0 +1 @@ +# 子表格 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/tag.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/tag.md new file mode 100644 index 0000000000..f927916b34 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/tag.md @@ -0,0 +1 @@ +# 标签 diff --git a/packages/core/client/docs/en-US/ui-schema/fields/association-components/title.md b/packages/core/client/docs/en-US/ui-schema/fields/association-components/title.md new file mode 100644 index 0000000000..e8a280714e --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/association-components/title.md @@ -0,0 +1 @@ +# 标题 \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/form-item.md b/packages/core/client/docs/en-US/ui-schema/fields/form-item.md new file mode 100644 index 0000000000..74112ffb22 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/form-item.md @@ -0,0 +1,22 @@ +# FormItem - 表单项 + +## UI Schema + +### 表单字段 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "string", + "x-designer": "FormItem.Designer", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "a.id", + "x-component-props": {}, + "x-read-pretty": true, + "x-uid": "u2nk80cguoa", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/en-US/ui-schema/fields/index.md b/packages/core/client/docs/en-US/ui-schema/fields/index.md new file mode 100644 index 0000000000..e734184dbe --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/index.md @@ -0,0 +1,5 @@ +# Overview + +## UI Schema 协议 + +- `x-collection-field` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/fields/table-column.md b/packages/core/client/docs/en-US/ui-schema/fields/table-column.md new file mode 100644 index 0000000000..61598b8f6a --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/fields/table-column.md @@ -0,0 +1,40 @@ +# 表格列 + +## UI Schema + +### 表格列 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "TableV2.Column.Decorator", + "x-designer": "TableV2.Column.Designer", + "x-component": "TableV2.Column", + "properties": { + "f_beilggsbj0r": { + "_isJSONSchemaObject": true, + "version": "2.0", + "x-collection-field": "a.f_beilggsbj0r", + "x-component": "CollectionField", + "x-component-props": { + "ellipsis": true + }, + "x-read-pretty": true, + "x-decorator": null, + "x-decorator-props": { + "labelStyle": { + "display": "none" + } + }, + "x-uid": "h5u4xi7uvji", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "z8p8ezctwsh", + "x-async": false, + "x-index": 2 +} +``` diff --git a/packages/core/client/docs/en-US/ui-schema/globals/menu.md b/packages/core/client/docs/en-US/ui-schema/globals/menu.md new file mode 100644 index 0000000000..69a4e6a800 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/globals/menu.md @@ -0,0 +1,127 @@ +# Menu + + +菜单模块以后会重构,Item 直接用 items 对接,不用 schema 嵌套,Menu 也会开放到更多场景里使用。 + + +## `x-initializer` + +- MenuItemInitializers + +## `x-designer` + +- Menu.Designer + +## `x-settings` + +- MenuSettings + +## UI Schema + +### 菜单 + +```json +{ + "type": "void", + "x-component": "Menu", + "x-designer": "Menu.Designer", + "x-initializer": "MenuItemInitializers", + "x-component-props": { + "mode": "mix", + "theme": "dark", + "onSelect": "{{ onSelect }}", + "sideMenuRefScopeKey": "sideMenuRef" + }, + "name": "lap9zil5z4u", + "x-uid": "x77wpukk3qz", + "x-async": false +} +``` + +### 菜单分组 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Group", + "x-component": "Menu.SubMenu", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "icon": "alipaycircleoutlined", + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "zjxw5sph3do", + "x-async": false, + "x-index": 1 +} +``` + +### 菜单页面 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Page1", + "x-component": "Menu.Item", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "icon": "alipaycircleoutlined", + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "y0jp661nb8i", + "x-async": false, + "x-index": 2 +} +``` + +### 链接 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Link", + "x-component": "Menu.URL", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "href": "#", + "icon": "alipaycircleoutlined" + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "ynembhzwcj1", + "x-async": false, + "x-index": 3 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/globals/page.md b/packages/core/client/docs/en-US/ui-schema/globals/page.md new file mode 100644 index 0000000000..2f93084f78 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/globals/page.md @@ -0,0 +1,94 @@ +# Page + + +现在页面依赖菜单,以后会独立出来,也可以创建无菜单的页面。另外,页面的标签页的设计还有问题。 + + +## `x-initializer` + +- PageTabsInitializers(暂无) +- BlockInitializers + +## `x-designer` + +- PageDesigner(暂无) +- PageTabsDesigner(暂无) + +## `x-settings` + +- PageSettings + +## UI Schema + +### 页面区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Page", + "title": "AA333", + "x-component-props": { + "enablePageTabs": true, + "hidePageTitle": true, + "disablePageHeader": true + }, + "properties": { + "nzxygx0iady": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "x-uid": "z8hxh8fxksb", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "9stdz1ld2p5", + "x-async": true, + "x-index": 1 +} +``` + +### 页面标签页 + +```json +{ + "x-uid": "cqq0ythy0yj", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Page", + "x-component-props": { + "enablePageTabs": true + }, + "properties": { + "o8gqaximg8c": { + "x-uid": "lzz2tqi8sxp", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "title": "Tab1", + "x-async": false, + "x-index": 1 + }, + "zjl1itzi6t8": { + "x-uid": "x10ru2grypq", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Tab2", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "x-async": false, + "x-index": 2 + } + }, + "x-async": true, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/en-US/ui-schema/globals/tabs.md b/packages/core/client/docs/en-US/ui-schema/globals/tabs.md new file mode 100644 index 0000000000..c3ccea4686 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/globals/tabs.md @@ -0,0 +1,5 @@ +# Tabs + + +标签页以后会重构,相关场景有页面的标签页和弹窗里的标签页 + diff --git a/packages/core/client/docs/en-US/ui-schema/index.md b/packages/core/client/docs/en-US/ui-schema/index.md new file mode 100644 index 0000000000..d908d9ed22 --- /dev/null +++ b/packages/core/client/docs/en-US/ui-schema/index.md @@ -0,0 +1,25 @@ +# 概述 + +## `x-initializer` + +不是所有节点都能使用 `x-initializer`,内置的 Schema 组件里,只有 Grid、ActionBar、Tabs、Table 组件有 `x-initializer` 参数 + +- Grid(通用) +- ActionBar(通用) +- Tabs(通用) +- Table +- Menu + +## `x-designer` + +`x-designer` 通常需要和 `BlockItem`、`FormItem`、`CardItem` 这类组件结合使用 + +完整的 Designer 组件有 + +- DragHandler +- Initializer +- Settings + +```tsx | pure + +``` diff --git a/packages/core/client/docs/index.md b/packages/core/client/docs/index.md deleted file mode 100644 index bc0930d2d0..0000000000 --- a/packages/core/client/docs/index.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/core/client/docs/intro.md b/packages/core/client/docs/intro.md deleted file mode 100644 index 526b115bae..0000000000 --- a/packages/core/client/docs/intro.md +++ /dev/null @@ -1,711 +0,0 @@ ---- -order: 1 ---- - -# 客户端内核 - - - -示例: - -```tsx -/** - * defaultShowCode: true - */ -import React from 'react'; -import { Link, Outlet } from 'react-router-dom'; -import { Application } from '@nocobase/client'; - -const Home = () =>

Home

; -const About = () =>

About

; - -const Layout = () => { - return
-
Home, About
- -
-} - -const app = new Application({ - router: { - type: 'memory', - initialEntries: ['/'] - } -}) - -app.router.add('root', { - element: -}) - -app.router.add('root.home', { - path: '/', - element: -}) - -app.router.add('root.about', { - path: '/about', - element: -}) - -export default app.getRootComponent(); -``` - -## SchemaComponent - -路由可以通过 JSON 的方式配置,可以注册诸多可供路由使用的组件模板,以方便各种场景支持,但是这些组件还是需要开发编写和维护,所以进一步将组件抽象,转换成配置化的方式。如: - -```tsx -/** - * defaultShowCode: true - * title: Schema Component - */ -import React from 'react'; -import { ISchema } from '@formily/react'; -import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client'; - -const schema: ISchema = { - name: 'hello', - 'x-component': 'Hello', - 'x-component-props': { - name: 'World', - }, -}; - -const Hello = ({ name }) =>

Hello {name}!

; - -export default function App() { - return ( - - - - ) -}; -``` - -可以通过 schema 方式配置的组件,称之为 schema 组件。在 SchemaComponentProvider 里注册各种 JSX 组件,编写相应的 schema,再通过 SchemaComponent 渲染。SchemaComponent 的 schema 就是 Formily 的 [Schema](https://react.formilyjs.org/zh-CN/api/shared/schema)。实际上 SchemaComponent 就是 Formily 的 [SchemaField](https://react.formilyjs.org/zh-CN/api/components/schema-field),之所以叫 SchemaComponent,是因为 SchemaComponent 可用于构建页面的各个部分,不局限于表单场景。 - -**思维转换:** - -虽然 Formily 的核心是致力于解决表单的复杂问题,但是随着不断的演变,已经不局限在表单层面了。Formily 核心提供了 Form 和 Field 两个非常重要的模型,Form 提供了路径系统和联动模型也同样适用于页面视图,Field 实际上也可以理解为组件,分为有值组件和无值组件两类,有值组件例如 Input、Select 等,无值组件例如 Drawer、Button 等。有值组件的数值又可以分不同类型:String、Number、Boolean、Object、Array 等等,无值组件没有数值,所以用 Void 表示,和 Formily 的 Field、ArrayField、ObjectField、VoidField 都对应上了。 - -为了适应动态的配置化表单解决方案,Formily 又提炼了 Schema 协议(DSL),这个协议完全适用于描述组件模型,用类 JSON Schema 的语法描述组件结构和对应的数值类型,这个 Schema 也是 SchemaComponent 的重要组成部分。 - -- Schema 是一个树结构,多个节点以树形结构连接起来,其中的一个 property 表示的就是其中的一个 Schema 节点。 -- 单 Schema 节点(不包括 properties)由核心 `x-component`、包装器 `x-decorator`、设计器 `x-designable` 三个组件构成。 - - `x-component` 核心组件 - - `x-decorator` 包装器,不同场景中,同一个核心组件,可能使用不同的包装器,如 FormItem、CardItem、BlockItem、Editable 等 - - `x-designable` 节点设计器(NocoBase 的扩展参数),一般为当前 schema 节点的配置表单。与 Formily 提供的 Designable 解决方案不同,`x-designable` 直接作用于当前 schema 节点,使用和配置不分离。 - -理论上,很多现有组件都可以直接转为 schema 组件,但是并不一定好用。以 Drawer 为例,常规 JSX 的写法一般是这样的: - -```tsx -/** - * defaultShowCode: true - * title: JSX Drawer - */ -import React, { useState } from 'react'; -import { Drawer, Button } from 'antd'; - -const App: React.FC = () => { - const [visible, setVisible] = useState(false); - const showDrawer = () => { - setVisible(true); - }; - const onClose = () => { - setVisible(false); - }; - return ( - <> - - 关闭 - } - > -

Some contents...

-

Some contents...

-

Some contents...

-
- - ); -}; - -export default App; -``` - -将组件转换成 schema,如果 1:1 转换是这样的: - -```tsx -/** - * defaultShowCode: true - * title: Drawer Schema - */ -import React, { useMemo } from 'react'; -import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client'; -import { Drawer as AntdDrawer, Button } from 'antd'; -import { createForm } from '@formily/core'; -import { RecursionField } from '@formily/react'; - -const Drawer = (props) => { - const { footerSchema, ...others } = props; - return ( - - } - {...others} - /> - ); -}; - -const schema = { - type: 'object', - properties: { - b1: { - type: 'void', - 'x-component': 'Button', - 'x-component-props': { - children: 'Open', - type: 'primary', - onClick: '{{showDrawer}}', - }, - }, - d1: { - type: 'void', - 'x-component': 'Drawer', - 'x-component-props': { - title: 'Basic Drawer', - onClose: '{{onClose}}', - footerSchema: { - type: 'object', - properties: { - fb1: { - type: 'void', - 'x-component': 'Button', - 'x-component-props': { - children: 'Close', - onClick: '{{onClose}}', - }, - }, - }, - }, - }, - }, - }, -}; - -export default function App() { - const form = useMemo(() => createForm(), []); - const showDrawer = () => { - form.query('d1').take((field) => { - field.componentProps.visible = true; - }); - }; - const onClose = () => { - form.query('d1').take((field) => { - field.componentProps.visible = false; - }); - }; - return ( - - - - ); -} -``` - -这个例子讲述了怎么将组件转换为可 Schema 配置,虽然达成了某种效果,但并不是一个很好的示例。 - -- 一个 property 就是一个 Schema 节点,Drawer 的 Schema 由平行的两个 schema 节点组成,不利于管理; -- 需要额外的自定义 scope 支持 drawer 组件 visible 的状态管理,而且这里自定义的 scope 复用性差; -- footer 需要特殊处理。在 x-component-props 里加了个 footerSchema 参数。但这个 footerSchema 并不是一个常规的 schema 节点,因为不是在 properties 里,不利于后端 schema 存储的统一规划; -- 删除 drawer,需要删除两个 schema 节点; -- 后端如何输出 drawer 这部分的 schema 也非常不方便,因为 drawer 由平行的两个节点组成。 - -为了解决上述问题,从结构上做了一些改良: - -```ts -{ - type: 'object', - properties: { - a1: { - 'x-component': 'Action', - title: 'Open', - properties: { - d1: { - 'x-component': 'Action.Drawer', - title: 'Drawer Title', - properties: { - c1: { - 'x-content': 'Hello', - }, - f1: { - 'x-component': 'Action.Drawer.Footer', - properties: { - a1: { - 'x-component': 'Action', - title: 'Close', - 'x-component-props': { - useAction: '{{ useCloseAction }}', - }, - }, - }, - }, - }, - }, - }, - }, - }, -} -``` - -以上示例,自定义了一个 Action 组件,用于配置按钮操作,又扩展了 Action.Drawer 和 Action.Drawer.Footer 两个特殊节点,分别用于配置抽屉弹框和抽屉的 footer。以上 schema 是个标准的组件树结构,层次十分分明。组件树的各个节点层次分明,schema 的增删改查就完全是标准流程了。 - -- 查询 Drawer 的 schema,只需要把 a1 节点的 json 全部输出就可以。 -- 修改各个节点和子节点都可以单节点独立处理,逻辑一致。 -- 删除时,直接删除不需要的节点就可以了。 -- 有利于扩展,比如继续增加 Action.Modal、Action.Modal.Footer 两个节点用于配置对话框。 - -Action.Drawer 完整的例子如下: - -```tsx -/** - * title: Action.Drawer - */ -import React, { createContext, useContext, useMemo, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { Button, Drawer } from 'antd'; -import { SchemaComponentProvider, SchemaComponent } from '@nocobase/client'; -import { observer, RecursionField, useField, useFieldSchema, ISchema } from '@formily/react'; - -const VisibleContext = createContext(null); - -const useA = () => { - return { - async run() {}, - }; -}; - -function useCloseAction() { - const [, setVisible] = useContext(VisibleContext); - return { - async run() { - setVisible(false); - }, - }; -} - -const Action: any = observer((props: any) => { - const { useAction = useA, onClick, ...others } = props; - const [visible, setVisible] = useState(false); - const schema = useFieldSchema(); - const field = useField(); - const { run } = useAction(); - return ( - - - - - ); -}, { displayName: 'Action' }); - -Action.Drawer = observer((props: any) => { - const [visible, setVisible] = useContext(VisibleContext); - const schema = useFieldSchema(); - const field = useField(); - return ( - <> - {createPortal( - setVisible(false)} - footer={ - { - return s['x-component'] === 'Action.Drawer.Footer'; - }} - /> - } - > - { - return s['x-component'] !== 'Action.Drawer.Footer'; - }} - /> - , - document.body, - )} - - ); -}, { displayName: 'Action.Drawer' }); - -Action.Drawer.Footer = observer((props: any) => { - const field = useField(); - const schema = useFieldSchema(); - return ; -}, { displayName: 'Action.Drawer.Footer' }); - -const schema: ISchema = { - type: 'object', - properties: { - a1: { - 'x-component': 'Action', - 'x-component-props': { - type: 'primary', - }, - title: 'Open', - properties: { - d1: { - 'x-component': 'Action.Drawer', - title: 'Drawer Title', - properties: { - c1: { - 'x-content': 'Hello', - }, - f1: { - 'x-component': 'Action.Drawer.Footer', - properties: { - a1: { - 'x-component': 'Action', - title: 'Close', - 'x-component-props': { - useAction: '{{ useCloseAction }}', - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; - -export default function App() { - return ( - - - - ); -} -``` - -## Designable - -SchemaComponent 基于 Formily 的 SchemaField,Formily 提供了 [Designable](https://github.com/alibaba/designable) 来解决 Schema 的配置问题,但是这套方案: - -- 需要维护两套代码,以 antd 为例,需要同时维护 @formily/antd 和 @designable/formily-antd 两套代码 -- 使用和设计分离,在设计器界面表单无法正常工作 - -另辟蹊径,NocoBase 构想了一种更为便捷的配置方案,使用和配置也可以兼顾,只需要维护一套代码。为此,提炼了一个简易的 `useDesignable()` Hook,可用于任意 Schema 组件中,动态配置 Schema,实时更新,实时渲染。 - -Hook API: - -```ts -const { - designable, // 是否可以配置 - patch, // 更新当前节点配置 - remove, // 移除当前节点 - insertAdjacent, // 在当前节点的相邻位置插入,四个位置:beforeBegin、afterBegin、beforeEnd、afterEnd - insertBeforeBegin, // 在当前节点的前面插入 - insertAfterBegin, // 在当前节点的第一个子节点前面插入 - insertBeforeEnd, // 在当前节点的最后一个子节点后面 - insertAfterEnd, // 在当前节点的后面 -} = useDesignable(); - -const schema = { - 'x-component': 'Hello', -}; - -// 在当前节点的前面插入 -insertBeforeBegin(schema); -// 等同于 -insertAdjacent('beforeBegin', schema); - -// 在当前节点的第一个子节点前面插入 -insertAfterBegin(schema); -// 等同于 -insertAdjacent('afterBegin', schema); - -// 在当前节点的最后一个子节点后面 -insertBeforeEnd(schema); -// 等同于 -insertAdjacent('beforeEnd', schema); - -// 在当前节点的后面 -insertAfterEnd(schema); -// 等同于 -insertAdjacent('afterEnd', schema); -``` - -insertAdjacent 的几个插入的位置: - -```ts -{ - properties: { - // beforeBegin 在当前节点的前面插入 - node1: { - properties: { - // afterBegin 在当前节点的第一个子节点前面插入 - // ... - // beforeEnd 在当前节点的最后一个子节点后面 - }, - }, - // afterEnd 在当前节点的后面 - }, -} -``` - -并不是所有场景都能使用 hook,所以提供了 `createDesignable()` 方法(实际上 `useDesignable()` 也是基于它来实现): - -```ts -const dn = createDesignable({ - current: schema, -}); - -dn.on('afterInsertAdjacent', (position, schema) => { - -}); - -dn.insertAfterEnd(schema); -``` - -相关例子如下: - - - -insertAdjacent 操作不仅可以用于新增节点,也可以用于现有节点的位置移动,如以下拖拽排序的例子: - -```tsx -/** - * title: 拖拽排序 - */ -import React from 'react'; -import { uid } from '@formily/shared'; -import { observer, useField, useFieldSchema } from '@formily/react'; -import { DndContext, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; -import { SchemaComponent, SchemaComponentProvider, createDesignable, useDesignable } from '@nocobase/client'; - -const useDragEnd = () => { - const { refresh } = useDesignable(); - - return ({ active, over }: DragEndEvent) => { - const activeSchema = active?.data?.current?.schema; - const overSchema = over?.data?.current?.schema; - - if (!activeSchema || !overSchema) { - return; - } - - const dn = createDesignable({ - current: overSchema, - }); - - dn.on('afterInsertAdjacent', refresh); - dn.insertBeforeBeginOrAfterEnd(activeSchema); - }; -}; - -const Page = observer((props) => { - return {props.children}; -}, { displayName: 'Page' }); - -function Draggable(props) { - const { attributes, listeners, setNodeRef, transform } = useDraggable({ - id: props.id, - data: props.data, - }); - const style = transform - ? { - transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, - } - : undefined; - - return ( - - ); -} - -function Droppable(props) { - const { isOver, setNodeRef } = useDroppable({ - id: props.id, - data: props.data, - }); - const style = { - color: isOver ? 'green' : undefined, - }; - - return ( -
- {props.children} -
- ); -} - -const Block = observer((props) => { - const field = useField(); - const fieldSchema = useFieldSchema(); - return ( - -
- Block {fieldSchema.name}{' '} - - Drag - -
-
- ); -}, { displayName: 'Block' }); - -export default function App() { - return ( - - - - ); -} -``` - -## APIClient - -在 WEB 应用里,客户端请求无处不在。为了便于客户端请求,提供的 API 有: - -- APIClient:客户端 SDK -- APIClientProvider:提供 APIClient 实例的 Context,全局共享 -- useRequest():需要结合 APIClientProvider 来使用 -- useApiClient():获取到当前配置的 apiClient 实例 - -```tsx | pure -const api = new APIClient({ - request, // 将 request 抛出去,方便各种自定义适配 -}); -api.request(options); -api.resource(name); - - - {/* children */} - -``` - -useRequest() 需要结合 APIClientProvider 一起使用,是对 ahooks 的 useRequest 的封装,支持 resource 请求。 - -```ts -const { data, loading } = useRequest(); -``` - - -## Providers - -客户端的扩展以 Providers 的形式存在,提供各种可供组件使用的 Context,可全局也可以局部使用。上文我们已经介绍了核心的三个 Providers: - -- SchemaComponentProvider,提供配置 Schema 所需的各种组件 -- ApiClientProvider,提供客户端 SDK - -除此之外,还有: - -- Router,实际也是 Provider,提供 History 的 Context,对应的有 BrowserRouter,HashRouter、MemoryRouter、NativeRouter、StaticRouter 几种可选方案 -- AntdConfigProvider,为 antd 组件提供统一的全局化配置 -- I18nextProvider,提供国际化解决方案 -- ACLProvider,提供权限配置,plugin-acl 的前端模块 -- CollectionManagerProvider,提供全局的数据表配置,plugin-collection-manager 的前端模块 -- SystemSettingsProvider,提供系统设置,plugin-system-settings 的前端模块 -- 其他扩展 - -多个 Providers 需要嵌套使用: - -```tsx | pure - - - {...} - - -``` - -但是这样的方式不利于 Providers 的管理和扩展,为此提炼了 `compose()` 函数用于配置多个 providers,如下: - - - -## Application - -上文例子的 Providers 还是差点意思,再进一步封装改造: - -```tsx | pure -const app = new Application({}); - -app.use(ApiClientProvider); -app.use([SchemaComponentProvider, { components: { Hello } }]); -app.use((props) => { - return ( -
- Home,About - -
- ); -}); - -app.mount('#root'); -// 等于 -ReactDOM.render(, document.getElementById('root')); -``` - -对比 NocoBase Server Application 中间件的核心实现: - -```ts -app.use((ctx, next) => {}); -const ctx = this.createContext(req, res) -await compose(app.middleware)(ctx); -await respond(ctx); -``` - -通过 app.use() 方法注册各种中间件插件,最后由 compose 来处理中间件,如果有需要也可以往 app.context 里添加各种东西待用。前端在处理 Provider 时也是类似的机制,这也是为什么客户端的扩展是以 Provider 的形式存在的原因。 - -从例子来看,以 Provider 的形式扩展是个不错的方案,但是还有两个问题没有解决: - -- Provider 的顺序怎么处理 -- 如何动态的加载前端模块 - -未完待续... diff --git a/packages/core/client/docs/zh-CN/core/application/application.md b/packages/core/client/docs/zh-CN/core/application/application.md new file mode 100644 index 0000000000..77d44d2880 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/application/application.md @@ -0,0 +1,369 @@ +# Application + +## new Application(options) + +创建一个 NocoBase 应用。 + +- 类型 + +```tsx | pure +export interface ApplicationOptions { + apiClient?: APIClientOptions; + ws?: WebSocketClientOptions | boolean; + i18n?: i18next; + providers?: (ComponentType | ComponentAndProps)[]; + plugins?: PluginType[]; + components?: Record; + scopes?: Record; + router?: RouterOptions; + schemaSettings?: SchemaSetting[]; + schemaInitializers?: SchemaInitializer[]; + loadRemotePlugins?: boolean; +} +``` + +- 详细信息 + - apiClient:API 请求实例,具体说明请参见:[https://docs.nocobase.com/api/sdk](https://docs.nocobase.com/api/sdk) + - i18n:国际化,具体请参考:[https://www.i18next.com/overview/api#createinstance](https://www.i18next.com/overview/api#createinstance) + - providers:上下文 + - components:全局组件 + - scopes:全局 scopes + - router:配置路由,具体请参考:[RouterManager](/core/application/router-manager) + - pluginSettings: [PluginSettingsManager](/core/application/plugin-settings-manager) + - schemaSettings:Schema 设置工具,具体参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) + - schemaInitializers:Schema 添加工具,具体参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) + - loadRemotePlugins:用于控制是否加载远程插件,默认为 `false`,即不加载远程插件(方便单测和 DEMO 环境)。 + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import { Application, Plugin } from '@nocobase/client'; + +const ProviderDemo = ({ children }) => { + return
+
hello world
+
{children}
+
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + } +} + +const app = new Application({ + providers: [ProviderDemo], + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +## 实例属性 + +### app.i18n + +```tsx | pure +class Application { + i18n: i18next; +} +``` + +详细介绍,请参考:[i18next](https://www.i18next.com/overview/api#createinstance) + +### app.apiClient + +```tsx | pure +class Application { + apiClient: APIClient; +} +``` + +详细介绍,请参考:[APIClient](https://docs.nocobase.com/api/sdk) + +### app.router + +详细介绍,请参考:[RouterManager](/core/application/router-manager) + +### app.pluginSettingsManager + +详细介绍,请参考:[PluginSettingsManager](/core/application/plugin-settings-manager) + +### app.schemaSettingsManager + +详细介绍,请参考:[SchemaSettingsManager](/core/ui-schema/schema-initializer-manager) + +### app.schemaInitializerManager + +详细介绍,请参考:[SchemaInitializerManager](/core/ui-schema/schema-initializer-manager) + +## 实例方法 + +### app.getRootComponent() + +获取应用的根组件。 + +- 类型 + +```tsx | pure +class Application { + getRootComponent(): React.FC +} +``` + +- 示例 + +```tsx | pure +import { Application } from '@nocobase/client'; + +const app = new Application(); + +const App = app.getRootComponent(); +``` + +### app.mount() + +将应用实例挂载在一个容器元素中。 + +- 类型 + +```tsx | pure +class Application { + mount(containerOrSelector: Element | ShadowRoot | string): ReactDOM.Root +} +``` + +- 示例 + +```tsx | pure +import { Application } from '@nocobase/client'; + +const app = new Application(); + +app.mount('#root'); +``` + +### app.addProvider() + +添加 `Provider` 上下文。 + +- 类型 + +```tsx | pure +class Application { + addProvider(component: ComponentType, props?: T): void; +} +``` + +- 详细信息 + +第一个参数是组件,第二个参数是组件的参数。注意 `Provider` 一定要渲染 `children`。 + +- 示例 + +```tsx | pure +// 场景1:第三方库,或者自己创建的 Context +const MyContext = createContext({}); +app.addProvider(MyContext.provider, { value: { color: 'red' } }); +``` + +```tsx +import { createContext, useContext } from 'react'; +import { Application, Plugin } from '@nocobase/client'; + +const MyContext = createContext(); + +const HomePage = () => { + const { color } = useContext(MyContext) || {}; + return
home page
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.addProvider(MyContext.Provider, { value: { color: 'red' } }); + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +```tsx | pure +// 场景2:自定义的组件,注意 children +const GlobalDemo = ({ name, children }) => { + return
+
hello, { name }
+
{ children }
+
+} +app.addProvider(GlobalDemo, { name: 'nocobase' }); +``` + + +```tsx +import { Application, Plugin } from '@nocobase/client'; + +const GlobalDemo = ({ name, children }) => { + return
+
hello, { name }
+
{ children }
+
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.addProvider(GlobalDemo, { name: 'nocobase' }); + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + + +### app.addProviders() + +添加多个 `Provider` 上下文。 + +- 类型 + +```tsx | pure +class Application { + addProviders(providers: (ComponentType | [ComponentType, any])[]): void; +} +``` + +- 详细信息 + +一次添加多个 `Provider`。 + +- 示例 + +```tsx | pure +app.addProviders([[MyContext.provider, { value: { color: 'red' } }], [GlobalDemo, { name: 'nocobase' }]]) +``` + +### app.addComponents() + +添加全局组件。 + +全局组件可以使用在 [RouterManager](/core/application/router-manager) 和 [UI Schema](/core/ui-schema/schema-component)上。 + +- 类型 + +```tsx | pure +class Application { + addComponents(components: Record): void; +} +``` + +- 示例 + +```tsx | pure +app.addComponents({ Demo, Foo, Bar }) +``` + +### app.addScopes() + +添加全局的 scope。 + +全局组 scope 可以 [UI Schema](/core/ui-schema/schema-component) 上。 + +- 类型 + +```tsx | pure +class Application { + addScopes(scopes: Record): void; +} +``` + +- 示例 + +```tsx | pure +function useSomeThing() {} +const anyVar = ''; + +app.addScopes({ useSomeThing, anyVar }) +``` + +## Hooks + +### useApp() + +获取当前应用的实例。 + +- 类型 + +```tsx | pure +const useApp: () => Application +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const app = useApp(); + return
{ JSON.stringify(app.router.getRouters()) }
+} +``` + +```tsx +import { Application, Plugin, useApp } from '@nocobase/client'; + +const HomePage = () => { + const app = useApp(); + return
{ JSON.stringify(app.router.getRoutes()) }
+} + +class MyPlugin extends Plugin { + async load(){ + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + } +} + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` diff --git a/packages/core/client/docs/zh-CN/core/application/plugin-manager.md b/packages/core/client/docs/zh-CN/core/application/plugin-manager.md new file mode 100644 index 0000000000..342420f32a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/application/plugin-manager.md @@ -0,0 +1,122 @@ +# PluginManager + +用于管理插件。 + +```tsx | pure +class PluginManager { + add(plugin: typeof Plugin, opts?: PluginOptions): Promise + + get(PluginClass: T): InstanceType; + get(name: string): T; +} +``` + +## 实例方法 + +### pluginManager.add() + +将插件添加到应用中。 + +- 类型 + +```tsx | pure +class PluginManager { + add(plugin: typeof Plugin, opts?: PluginOptions): Promise +} +``` + +- 详细信息 + +第一个参数是插件类,第二个则是实例化时传递的参数。前面已经讲过会在添加插件后,立即调用 `afterAdd` 钩子函数,所以返回的是 `Promise`。 + +对于远程组件而言,会自动传递一个 `name` 参数。 + +- 示例 + +```tsx | pure +class MyPluginA extends Plugin { + async load() { + console.log('options', this.options) + console.log('app', this.app); + console.log('router', this.app.router, this.router); + } +} + +class MyPluginB extends Plugin { + // 需要在 afterAdd 执行添加的方法 + async afterAdd() { + // 通过 `app.pluginManager.add()` 添加插件时,第一个参数是插件类,第二个参数是实例化时传递的参数 + this.app.pluginManager.add(MyPluginA, { name: 'MyPluginA', hello: 'world' }) + } +} + +const app = new Application({ + plugins: [MyPluginB], +}); +``` + +### pluginManager.get() + +获取插件实例。 + +- 类型 + +```tsx | pure +class PluginManager { + get(PluginClass: T): InstanceType; + get(name: string): T; +} +``` + +- 详细信息 + +可以通过 Class 获取插件示例,如果在插件注册的时候有 name,也可以通过字符串的 name 获取。 + +如果是远程插件,会自动传入 name,值为 package 的 name。 + +- 示例 + +```tsx | pure +import MyPluginA from 'xxx'; + +class MyPluginB extends Plugin { + async load() { + // 方式1:通过 Class 获取 + const myPluginA = this.app.pluginManager.get(MyPluginA); + + // 方式2:通过 name 获取(添加的时候要传递 name 参数) + const myPluginA = this.app.pluginManager.get('MyPluginA'); + } +} +``` + +## Hooks + +获取插件实例,等同于 `pluginManager.get()`。 + +### usePlugin() + +```tsx | pure +function usePlugin(plugin: T): InstanceType; +function usePlugin(name: string): T; +``` + +- 详细信息 + +可以通过 Class 获取插件示例,如果在插件注册的时候有 name,也可以通过字符串的 name 获取。 + +- 示例 + +```tsx | pure +import { usePlugin } from '@nocobase/client'; + +const Demo = () => { + // 通过 Class 获取 + const myPlugin = usePlugin(MyPlugin); + + // 通过 name 获取(添加的时候要传递 name 参数) + const myPlugin = usePlugin('MyPlugin'); + + return
+} +``` diff --git a/packages/core/client/docs/zh-CN/core/application/plugin-settings-manager.md b/packages/core/client/docs/zh-CN/core/application/plugin-settings-manager.md new file mode 100644 index 0000000000..34e6f8c67d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/application/plugin-settings-manager.md @@ -0,0 +1,269 @@ +# PluginSettingsManager + +![](../static/plugin-settings.jpg) + +用于管理插件配置页面,其底层对应着 [RouterManager](/core/application/router-manager)。 + +```tsx | pure +interface PluginSettingOptionsType { + title: string; + /** + * @default `Outlet` + */ + Component?: ComponentType | string; + icon?: string; + /** + * sort, the smaller the number, the higher the priority + * @default 0 + */ + sort?: number; + aclSnippet?: string; +} + +interface PluginSettingsPageType { + label?: string; + title: string; + key: string; + icon: any; + path: string; + sort?: number; + name?: string; + isAllow?: boolean; + topLevelName?: string; + aclSnippet: string; + children?: PluginSettingsPageType[]; +} + +class PluginSettingsManager { + add(name: string, options: PluginSettingOptionsType): void + get(name: string, filterAuth?: boolean): PluginSettingsPageType; + getList(filterAuth?: boolean): PluginSettingsPageType[] + has(name: string): boolean; + remove(name: string): void; + getRouteName(name: string): string + getRoutePath(name: string): string; + hasAuth(name: string): boolean; +} +``` + +## 实例方法 + +### pluginSettingsManager.add() + +添加插件配置页。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + add(name: string, options: PluginSettingOptionsType): void +} +``` + +- 详细解释 + +第一个参数 `name`,是路由唯一标识,用于后续的删改查,并且 `name` 支持 `.` 用于分割层级,不过需要注意当使用 `.` 分层的时候,父级要使用 [Outlet](https://reactrouter.com/en/main/components/outlet),让子元素能正常渲染。 + +第二个参数中 `Component` 支持组件形式和字符串形式,如果是字符串组件,要先通过 [app.addComponents](/core/application/application#appaddcomponents) 进行注册,具体参考 [RouterManager](/core/application/router-manager)。 + +- 示例 + +单层级配置。 + +```tsx | pure +const HelloSettingPage = () => { + return
hello setting page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.add('hello', { + title: 'Hello', // menu title and page title + icon: 'ApiOutlined', // menu icon + Component: HelloSettingPage + }) + } +} +``` + +多层级配置。 + +```tsx | pure +// 多层级配置页 + +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.add('hello', { + title: 'HelloWorld', + icon: '', + // Component: Outlet, 默认为 react-router-dom 的 Outlet 组件,可自定义 + }) + + this.app.pluginSettingsManager.add('hello.demo1', { + title: 'Demo1 Page', + Component: () =>
Demo1 Page Content
+ }) + + this.app.pluginSettingsManager.add('hello.demo2', { + title: 'Demo2 Page', + Component: () =>
Demo2 Page Content
+ }) + } +} +``` + +### pluginSettingsManager.get() + +获取配置信息。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + get(name: string, filterAuth?: boolean): PluginSettingsPageType; +} +``` + +- 详细解释 + +第一个是在添加时的 name 参数,第二个参数是是否在获取的时候进行权限过滤。 + +- 示例 + +在组件中获取。 + +```tsx | pure +const Demo = () => { + const app = useApp(); + const helloSettingPage = this.app.pluginSettingsManager.get('hello'); +} +``` + +在插件中获取。 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const helloSettingPage = this.app.pluginSettingsManager.get('hello') + const helloSettingPage = this.app.pluginSettingsManager.get('hello', false); + + const mobileAppConfigPage = this.app.pluginSettingsManager.get('mobile.app') + } +} +``` + +### pluginSettingsManager.getList() + +获取插件配置页列表。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getList(filterAuth?: boolean): PluginSettingsPageType[] +} +``` + +- 详细解释 + +`filterAuth` 默认值为 `true`,即进行权限过滤。 + +- 示例 + +```tsx | pure +const Demo = () => { + const app = useApp(); + const settings = app.pluginSettingsManager.getList(); + const settings = app.pluginSettingsManager.getList(false); +} +``` + +### pluginSettingsManager.has() + +判断是否存在,内部已进行权限过滤。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + has(name: string): boolean; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.pluginSettingsManager.has('hello'); + } +} +``` + +### pluginSettingsManager.remove() + +移除配置。 + +```tsx | pure +class PluginSettingsManager { + remove(name: string): void; +} +``` + +### pluginSettingsManager.getRouteName() + +获取对应路由的名称。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getRouteName(name: string): string +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const helloRouteName = this.pluginSettingsManager.getRouteName('hello'); // admin.settings.hello + } +} +``` + +### pluginSettingsManager.getRoutePath() + +获取插件配置对应的页面路径。 + +- 类型 + +```tsx | pure +class PluginSettingsManager { + getRoutePath(name: string): string; +} +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const navigate = useNavigate(); + const app = useApp(); + const helloSettingPath = app.pluginSettingsManager.getRoutePath('hello'); + + return
navigate(helloSettingPath)}> + go to hello setting page +
+} +``` + +### pluginSettingsManager.hasAuth() + +单独判断是否权限。 + +```tsx | pure +class PluginSettingsManager { + hasAuth(name: string): boolean; +} +``` diff --git a/packages/core/client/docs/zh-CN/core/application/plugin.md b/packages/core/client/docs/zh-CN/core/application/plugin.md new file mode 100644 index 0000000000..839bfb964d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/application/plugin.md @@ -0,0 +1,85 @@ +# Plugin + +Plugin 基类。 + +- 类型 + +```tsx | pure +class Plugin { + constructor( + protected options: T, + protected app: Application, + ) { + this.options = options; + this.app = app; + } + + get pluginManager() { + return this.app.pluginManager; + } + + get router() { + return this.app.router; + } + + get pluginSettingsManager() { + return this.app.pluginSettingsManager; + } + + get schemaInitializerManager() { + return this.app.schemaInitializerManager; + } + + get schemaSettingsManager() { + return this.app.schemaSettingsManager; + } + + async afterAdd() {} + + async beforeLoad() {} + + async load() {} +} +``` + +- 详细信息 + + - 构造函数 + - `options`: 插件的添加有两种方式,一种方式从插件列表中远程加载出来,另一种方式是通过 [PluginManager](/core/application/plugin-manager) 添加 + - 远程加载:`options` 会被自动注入 `{ name: 'npm package.name' }` + - PluginManager `options` 由用户自己传递 + - `app`:此参数是自动注入的,是应用实例 + - 快捷访问:基类提供了对 `app` 部分方法和属性的快捷访问 + - `pluginManager` + - `router` + - `pluginSettingsManager` + - `schemaSettingsManager` + - `schemaInitializerManager` + - 声明周期 + - `afterAdd`:插件被添加后立即执行 + - `beforeLoad`:执行渲染时执行,在 `afterAdd` 之后,`load` 之前 + - `load`:最后执行 +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + + async afterAdd() { + console.log('afterAdd') + } + + async beforeLoad() { + console.log('beforeLoad') + } + + async load() { + console.log('load') + + // 可以访问应用实例 + console.log(this.app) + + // 访问应用实例内容 + console.log(this.app.router, this.router); + } +} +``` diff --git a/packages/core/client/docs/zh-CN/core/application/router-manager.md b/packages/core/client/docs/zh-CN/core/application/router-manager.md new file mode 100644 index 0000000000..ef707219c8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/application/router-manager.md @@ -0,0 +1,386 @@ +# RouterManager + +用于管理路由。 + +```tsx | pure +import { ComponentType } from 'react'; +import { RouteObject } from 'react-router-dom'; + +interface RouteType extends Omit { + Component?: ComponentType | string; +} + +class RouterManager { + add(name: string, route: RouteType): void; + getRoutes(): Record; + getRoutesTree(): RouteObject[]; + get(name: string): RouteType; + has(name: string): boolean; + remove(name: string): void; + setType(type: 'browser' | 'memory' | 'hash'): void; + setBasename(basename: string): void; +} +``` + +## 实例方法 + +### router.add() + +添加一条路由。 + +- 类型 + +```tsx | pure +class RouterManager { + add(name: string, route: RouteType): void +} +``` + +- 详情 + +第一个参数 `name`,是路由唯一标识,用于后续的删改查,并且 `name` 支持 `.` 用于分割层级,不过需要注意当使用 `.` 分层的时候,父级要使用 [Outlet](https://reactrouter.com/en/main/components/outlet),让子元素能正常渲染。 + +第二个参数 `RouteType` 的 `Component` 支持组件形式和字符串形式,如果是字符串组件,要先通过 [app.addComponents](/core/application/application#appaddcomponents) 进行注册。 + +- 示例 + +单层级路由。 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.add('home', { + path: '/', + Component: () =>
home page
+ }) + this.app.router.add('login', { + path: '/login', + element:
login page
+ }) + } +} +``` + +```tsx +import { useNavigate } from 'react-router-dom'; +import { Plugin, Application } from '@nocobase/client'; + +const HomePage = () => { + const navigate = useNavigate(); + return
+
home page
+ +
+} + +const LoginPage = () => { + const navigate = useNavigate(); + return
+
login page
+ +
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('home', { + path: '/', + Component: HomePage + }) + this.app.router.add('login', { + path: '/login', + Component: LoginPage + }) + } +} + + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/'], + } +}); + +export default app.getRootComponent(); +``` + +多层级路由。 + +```tsx | pure +import { Plugin } from '@nocobase/client'; +import { Outlet } from 'react-router-dom'; + +const AdminLayout = () =>{ + return
+
This is admin layout
+ +
+} + +const AdminSettings = () => { + return
This is admin settings page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('admin', { + path: '/admin', + Component: AdminLayout + }) + this.app.router.add('admin.settings', { + path: '/admin/settings', + Component: AdminSettings , + }) + } +} +``` + + +```tsx +import { useNavigate, Outlet } from 'react-router-dom'; +import { Plugin, Application } from '@nocobase/client'; + +const AdminLayout = () =>{ + return
+
This is admin layout
+ +
+} + +const AdminSettings = () => { + return
This is admin settings page
+} + +class MyPlugin extends Plugin { + async load() { + this.app.router.add('admin', { + path: '/admin', + Component: AdminLayout + }) + this.app.router.add('admin.settings', { + path: '/admin/settings', + Component: AdminSettings , + }) + } +} + + +const app = new Application({ + plugins: [MyPlugin], + router: { + type: 'memory', + initialEntries: ['/admin/settings'], + } +}); + +export default app.getRootComponent(); +``` + +`Component` 参数为字符串。 + +```tsx | pure +const LoginPage = () => { + return
login page
+} + +class MyPlugin extends Plugin { + async load() { + // 通过 app.addComponents 进行注册 + this.app.addComponents({ LoginPage }) + + this.app.router.add('login', { + path: '/login', + Component: 'LoginPage', // 这里可以使用字符串了 + }) + } +} +``` + +### router.getRoutes() + +获取路由列表。 + +- 类型 + +```tsx | pure +class RouterManager { + getRoutes(): Record +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + console.log(this.app.router.getRoutes()); + } +} +``` + +![](../static/CxjJbp0pcogYn6xviVyc6QT8nUg.png) + +### router.getRoutesTree() + +获取用于 [useRoutes()](https://reactrouter.com/hooks/use-routes) 的数据。 + +- 类型 + +```tsx | pure +class RouterManager { + getRoutesTree(): RouteObject[] +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const routes = this.app.router.getRoutesTree(); + } +} +``` + +### router.get() + +获取单个路由配置。 + +- 类型 + +```tsx | pure +class RouterManager { + get(name: string): RouteType +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const adminRoute = this.app.router.get('admin') + const adminSettings = this.app.router.get('admin.settings') + } +} +``` + +### router.has() + +判断是否添加过路由。 + +- 类型 + +```tsx | pure +class RouterManager { + has(name: string): boolean; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasAdminRoute = this.app.router.has('admin') + const hasAdminSettings = this.app.router.has('admin.settings') + } +} +``` + +### router.remove() + +移除路由配置。 + +- 类型 + +```tsx | pure +class RouterManager { + remove(name: string): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.remove('admin') + this.app.router.remove('admin.settings') + } +} +``` + +### router.setType() + +设置路由类型,默认为 `browser`。 + + +- 类型 + +```tsx | pure +class RouterManager { + setType(type: 'browser' | 'memory' | 'hash'): void; +} +``` + +- 详细解释 + - browser: [BrowserRouter](https://reactrouter.com/en/main/router-components/browser-router) + - memory: [MemoryRouter](https://reactrouter.com/en/main/router-components/hash-router) + - hash: [HashRouter](https://reactrouter.com/en/main/router-components/memory-router) + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.setType('hash') + } +} +``` + +### router.setBasename() + +设置 [basename](https://reactrouter.com/en/main/router-components/browser-router#basename)。 + +- 类型 + +```tsx | pure +class RouterManager { + setBasename(basename: string): void; +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.router.setBasename('/') + } +} +``` + +## Hooks + +### useRouter() + +获取当前路由的实例,等同于 `app.router`。 + +- 类型 + +```tsx | pure +const useRouter: () => RouterManager +``` + +- 示例 + +```tsx | pure +import { useRouter } from '@nocobase/client'; + +const Demo = () => { + const router = useRouter(); +} +``` diff --git a/packages/core/client/docs/zh-CN/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png b/packages/core/client/docs/zh-CN/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png new file mode 100644 index 0000000000..0fa567bf5f Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/static/CxjJbp0pcogYn6xviVyc6QT8nUg.png differ diff --git a/packages/core/client/docs/zh-CN/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png b/packages/core/client/docs/zh-CN/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png new file mode 100644 index 0000000000..d27e675f70 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/static/KTUWb69kioUg8bxYTAMc2ReDnRg.png differ diff --git a/packages/core/client/docs/zh-CN/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png b/packages/core/client/docs/zh-CN/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png new file mode 100644 index 0000000000..9d65a782cb Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/static/VfPGbhWo9os0qTxcITkcHNfin4g.png differ diff --git a/packages/core/client/docs/zh-CN/core/static/designer.png b/packages/core/client/docs/zh-CN/core/static/designer.png new file mode 100644 index 0000000000..466fa4cfd8 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/static/designer.png differ diff --git a/packages/core/client/docs/zh-CN/core/static/plugin-settings.jpg b/packages/core/client/docs/zh-CN/core/static/plugin-settings.jpg new file mode 100644 index 0000000000..34a8da1162 Binary files /dev/null and b/packages/core/client/docs/zh-CN/core/static/plugin-settings.jpg differ diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx new file mode 100644 index 0000000000..40a0c5a297 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-basic.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-common.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-common.tsx new file mode 100644 index 0000000000..79962078e3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-common.tsx @@ -0,0 +1,38 @@ +import { ApplicationOptions, Grid, Plugin, SchemaComponent } from '@nocobase/client'; +import React from 'react'; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid }); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const appOptions: ApplicationOptions = { + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}; + +export { appOptions }; diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx new file mode 100644 index 0000000000..6427050686 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-component.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { Avatar } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + insertPosition: 'beforeEnd', + Component: (props: any) => ( + + C + + ), + componentProps: { + size: 'large', + }, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-divider.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-divider.tsx new file mode 100644 index 0000000000..39a23be52e --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-divider.tsx @@ -0,0 +1,57 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'd', + type: 'divider', + }, + { + name: 'b', + title: 'B Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'B1', + }, + { + name: 'a2', + type: 'item', + title: 'B2', + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx new file mode 100644 index 0000000000..d691ff9259 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-group.tsx @@ -0,0 +1,71 @@ +import { Application, SchemaInitializer, SchemaInitializerItemGroup } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; +import React from 'react'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A Group Title', + type: 'itemGroup', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'b', + title: 'B Group Title', + type: 'itemGroup', + divider: true, // 渲染分割线 + useChildren() { + // 动态子元素 + return [ + { + name: 'b1', + type: 'item', + title: 'B1', + }, + { + name: 'b2', + type: 'item', + title: 'B2', + }, + ]; + }, + }, + { + name: 'c', + Component: () => { + return ( + + {[ + { + name: 'c1', + type: 'item', + title: 'C1', + }, + ]} + + ); + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-item.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-item.tsx new file mode 100644 index 0000000000..825e81586a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-item.tsx @@ -0,0 +1,88 @@ +/** + * defaultShowCode: true + */ +import { + Grid, + SchemaInitializer, + useSchemaInitializer, + SchemaInitializerItem, + CardItem, + Application, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; +import { useFieldSchema } from '@formily/react'; + +const Demo = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + Component: Demo, + }, + { + name: 'demo2', + type: 'item', + useComponentProps() { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + + return { + title: 'type 方式定义', + onClick: handleClick, + }; + }, + }, + { + name: 'demo3', + title: 'with items', + type: 'item', + onClick(args) { + console.log(args); + }, + items: [ + { + label: 'aaa', + value: 'aaa', + }, + { + label: 'bbb', + value: 'bbb', + }, + ], + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], + components: { CardItem, Hello }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx new file mode 100644 index 0000000000..f795b02715 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-menu.tsx @@ -0,0 +1,53 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'A subMenu', + type: 'subMenu', + children: [ + { + name: 'a1', + type: 'item', + title: 'A1', + }, + { + name: 'a2', + type: 'item', + title: 'A2', + }, + ], + }, + { + name: 'b', + title: 'B subMenu', + type: 'subMenu', + children: [ + { + name: 'a1', + type: 'item', + title: 'B1', + }, + { + name: 'a2', + type: 'item', + title: 'B2', + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-select.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-select.tsx new file mode 100644 index 0000000000..69590dab4f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-select.tsx @@ -0,0 +1,53 @@ +/** + * defaultShowCode: true + */ +import { Grid, SchemaInitializer, Application, SchemaInitializerSelect, useDesignable } from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; +import { useFieldSchema } from '@formily/react'; + +const OpenModeSelect = () => { + const fieldSchema = useFieldSchema(); + const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer'; + + const { patch } = useDesignable(); + const handleChange = (value) => { + // 修改当前节点的 Schema 的属性 + patch({ + 'x-component-props': { + openMode: value, + }, + }); + }; + + return ( + + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'openMode', + Component: OpenModeSelect, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-switch.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-switch.tsx new file mode 100644 index 0000000000..c8eb33888d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-components-switch.tsx @@ -0,0 +1,88 @@ +/** + * defaultShowCode: true + */ +import { + Grid, + SchemaInitializer, + Application, + SchemaInitializerSwitch, + useCurrentSchema, + useSchemaInitializer, + Action, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-initializer-common'; + +const actionKey = 'x-action'; + +const schema = { + type: 'void', + [actionKey]: 'create', + title: "{{t('Add new')}}", + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + }, +}; + +const AddNewButton = () => { + // 判断是否已插入 + const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey); + + const { insert } = useSchemaInitializer(); + return ( + { + // 如果已插入,则移除 + if (exists) { + return remove(); + } + // 新插入子节点 + insert(schema); + }} + /> + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Configure actions', + wrap: Grid.wrap, + items: [ + { + name: 'Add New', + Component: AddNewButton, + }, + { + name: 'Add New2', + type: 'switch', + useComponentProps() { + const { exists, remove } = useCurrentSchema(schema[actionKey], actionKey); + + const { insert } = useSchemaInitializer(); + return { + checked: exists, + title: 'Add new - type 方式', + onClick() { + // 如果已插入,则移除 + if (exists) { + return remove(); + } + // 新插入子节点 + insert(schema); + }, + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], + components: { Action }, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-hooks-item.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-hooks-item.tsx new file mode 100644 index 0000000000..ddf54ed2d4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-hooks-item.tsx @@ -0,0 +1,33 @@ +/** + * defaultShowCode: true + */ +import React, { ReactNode } from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem, useSchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; +import { TableOutlined } from '@ant-design/icons'; + +const Demo = () => { + const { name, foo, icon } = useSchemaInitializerItem<{ name: string; foo: string; icon: ReactNode }>(); + + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + foo: 'bar', + icon: , + Component: Demo, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx new file mode 100644 index 0000000000..9c7ae48c33 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-items.tsx @@ -0,0 +1,143 @@ +import React, { FC } from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerChild, + SchemaInitializerItem, + SchemaInitializerItemsProps, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { ButtonProps, ListProps, List, Card } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const CustomListGridMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ( + + + + )} + > + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + ItemsComponent: CustomListGridMenu, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-children.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-children.tsx new file mode 100644 index 0000000000..1e7fe3e234 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-children.tsx @@ -0,0 +1,61 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + type: 'itemGroup', + title: '静态 children', + children: [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ], + }, + { + name: 'a', + type: 'itemGroup', + title: '动态 children', + useChildren() { + return [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ]; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-define.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-define.tsx new file mode 100644 index 0000000000..534ec581b3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-define.tsx @@ -0,0 +1,34 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + }, + { + name: 'b', + type: 'item', // 通过 `type` 定义,底层对应着 `SchemaInitializerItem` 组件 + title: 'type Demo', + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-props.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-props.tsx new file mode 100644 index 0000000000..37d46fa4ed --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-props.tsx @@ -0,0 +1,40 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaInitializer, SchemaInitializerItem } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const CommonDemo = (props) => { + return ; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + Component: CommonDemo, + componentProps: { + title: 'componentProps', + }, + }, + { + name: 'b', + Component: CommonDemo, + useComponentProps() { + return { + title: 'useComponentProps', + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-visible.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-visible.tsx new file mode 100644 index 0000000000..ead52953d1 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-options-item-visible.tsx @@ -0,0 +1,32 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaInitializer } from '@nocobase/client'; +import { appOptions } from './schema-initializer-common'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + type: 'item', + title: 'Demo A', + }, + { + name: 'b', + type: 'item', + title: 'Demo B', + useVisible() { + return false; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaInitializers: [myInitializer], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx new file mode 100644 index 0000000000..a5890ba9fb --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-popover.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + useDesignable, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; +import { Button } from 'antd'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +const MyInitializerComponent = () => { + const { insertBeforeEnd } = useDesignable(); + return ( + + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + popover: false, + Component: MyInitializerComponent, +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-render.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-render.tsx new file mode 100644 index 0000000000..214a17cec8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-initializer-render.tsx @@ -0,0 +1,127 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import { Field } from '@formily/core'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return ; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const fieldSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer']); + return ( +
+
{render()}
+
可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}
+
+ ); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], + designable: true, +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-common.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-common.tsx new file mode 100644 index 0000000000..01b99b9aaa --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-common.tsx @@ -0,0 +1,48 @@ +import { ApplicationOptions, CardItem, Grid, Plugin, SchemaComponent } from '@nocobase/client'; +import React from 'react'; + +const Hello = () => { + return

Hello, world!

; +}; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const appOptions: ApplicationOptions = { + router: { + type: 'memory', + }, + designable: true, + components: { Grid, CardItem, Hello }, + plugins: [PluginHello], +}; + +export { appOptions }; diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-action-modal.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-action-modal.tsx new file mode 100644 index 0000000000..1a7076c923 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-action-modal.tsx @@ -0,0 +1,66 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { + Application, + FormItem, + Input, + SchemaSettings, + SchemaSettingsActionModalItem, + useDesignable, +} from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { ISchema, useField } from '@formily/react'; + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const filed = useField(); + const { patch } = useDesignable(); + + return ( + { + patch({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ FormItem, Input }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-divider.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-divider.tsx new file mode 100644 index 0000000000..f2b3d98b66 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-divider.tsx @@ -0,0 +1,68 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'group A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'b', + type: 'itemGroup', + componentProps: { + title: 'group B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-group.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-group.tsx new file mode 100644 index 0000000000..e47b99a135 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-group.tsx @@ -0,0 +1,64 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'group A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'b', + type: 'itemGroup', + componentProps: { + title: 'group B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-item.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-item.tsx new file mode 100644 index 0000000000..376ff77018 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-item.tsx @@ -0,0 +1,76 @@ +/** + * defaultShowCode: true + */ +import React, { FC, useState } from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Button, Input, Space } from 'antd'; + +const MarkdownEdit = () => { + const field = useField(); + return ( + { + field.editable = true; + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'markdown', + Component: MarkdownEdit, + }, + ], +}); + +const Hello: FC<{ content?: string }> = observer((props) => { + const field = useField(); + const { content } = props; + const [inputVal, setInputVal] = useState(content); + const { patch } = useDesignable(); + return field.editable ? ( + + setInputVal(e.target.value)} /> + + + + + + ) : ( +

{content}

+ ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-modal.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-modal.tsx new file mode 100644 index 0000000000..2f35e88872 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-modal.tsx @@ -0,0 +1,66 @@ +/** + * defaultShowCode: true + */ +import { ISchema } from '@formily/react'; +import { + Application, + FormItem, + Input, + SchemaSettings, + SchemaSettingsModalItem, + useSchemaSettings, +} from '@nocobase/client'; +import React from 'react'; +import { appOptions } from './schema-settings-common'; + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const { dn } = useSchemaSettings(); + + return ( + { + dn.deepMerge({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ FormItem, Input }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-remove.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-remove.tsx new file mode 100644 index 0000000000..c1b101cc30 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-remove.tsx @@ -0,0 +1,28 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn(s) { + return s['x-component'] === 'Grid'; // 其顶级是 Grid,这一层级不能删 + }, + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-select.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-select.tsx new file mode 100644 index 0000000000..c14dfaff40 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-select.tsx @@ -0,0 +1,82 @@ +/** + * defaultShowCode: true + */ +import React, { FC } from 'react'; +import { Application, SchemaSettings, SchemaSettingsSelectItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Table } from 'antd'; + +const PageSize = () => { + const { patch } = useDesignable(); + const filed = useField(); + return ( + { + patch({ + 'x-component-props': { + pageSize: v, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'pageSize', + Component: PageSize, + }, + ], +}); + +const data = []; +for (let i = 0; i < 46; i++) { + data.push({ + key: i, + name: `Edward King ${i}`, + age: 32, + address: `London, Park Lane no. ${i}`, + }); +} +const Hello: FC<{ pageSize: number }> = observer((props) => { + return ( +
+ ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-sub-menu.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-sub-menu.tsx new file mode 100644 index 0000000000..4e9be9478c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-sub-menu.tsx @@ -0,0 +1,64 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'subMenu', + componentProps: { + title: 'Sub Menu A', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A1', + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A2', + }, + }, + ], + }, + { + name: 'b', + type: 'subMenu', + componentProps: { + title: 'Sub Menu B', + }, + children: [ + { + name: 'b1', + type: 'item', + componentProps: { + title: 'B1', + }, + }, + { + name: 'b2', + type: 'item', + componentProps: { + title: 'B2', + }, + }, + ], + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-switch.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-switch.tsx new file mode 100644 index 0000000000..7569ecddf3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-components-switch.tsx @@ -0,0 +1,74 @@ +/** + * defaultShowCode: true + */ +import React, { FC } from 'react'; +import { Application, SchemaSettings, SchemaSettingsSwitchItem, useDesignable } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; +import { observer, useField } from '@formily/react'; +import { Form, Input } from 'antd'; + +const FormItemRequired = () => { + const { patch } = useDesignable(); + const filed = useField(); + return ( + { + patch({ + 'x-component-props': { + required: v, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'required', + Component: FormItemRequired, + }, + { + name: 'required2', + type: 'switch', + useComponentProps() { + const { patch } = useDesignable(); + const filed = useField(); + return { + title: 'Required - type 方式', + checked: !!filed.componentProps?.required, + onChange(v) { + patch({ + 'x-component-props': { + required: v, + }, + }); + }, + }; + }, + }, + ], +}); + +const Hello: FC<{ required: boolean }> = observer((props) => { + return ( + + + + + + ); +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +app.addComponents({ Hello }); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx new file mode 100644 index 0000000000..45fba8c3ba --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-children.tsx @@ -0,0 +1,68 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: '静态 children', + }, + children: [ + { + name: 'a1', + type: 'item', + componentProps: { + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + }, + { + name: 'a2', + type: 'item', + componentProps: { + title: 'A 2', + }, + }, + ], + }, + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: '动态 children', + }, + useChildren() { + return [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ]; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-define.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-define.tsx new file mode 100644 index 0000000000..f370a23ee1 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-define.tsx @@ -0,0 +1,35 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return ; +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + }, + { + name: 'b', + type: 'item', // 通过 `type` 定义,底层对应着 `SchemaInitializerItem` 组件 + componentProps: { + title: 'type Demo', + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-props.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-props.tsx new file mode 100644 index 0000000000..7b37376111 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-props.tsx @@ -0,0 +1,39 @@ +/** + * defaultShowCode: true + */ +import React from 'react'; +import { Application, SchemaSettings, SchemaSettingsItem } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const CommonDemo = (props) => { + return ; +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: CommonDemo, + componentProps: { + title: 'componentProps', + }, + }, + { + name: 'b', + Component: CommonDemo, + useComponentProps() { + return { + title: 'useComponentProps', + }; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-visible.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-visible.tsx new file mode 100644 index 0000000000..6cfc7f725f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-options-item-visible.tsx @@ -0,0 +1,35 @@ +/** + * defaultShowCode: true + */ +import { Application, SchemaSettings } from '@nocobase/client'; +import { appOptions } from './schema-settings-common'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'item', + componentProps: { + title: 'Demo A', + }, + }, + { + name: 'b', + type: 'item', + componentProps: { + title: 'Demo B', + }, + useVisible() { + return false; + }, + }, + ], +}); + +const app = new Application({ + ...appOptions, + schemaSettings: [mySettings], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-render.tsx b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-render.tsx new file mode 100644 index 0000000000..089b17cd29 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/demos/schema-settings-render.tsx @@ -0,0 +1,150 @@ +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + FormItem, + Grid, + Input, + Plugin, + SchemaComponent, + SchemaSettings, + SchemaSettingsModalItem, + createDesignable, + useAPIClient, + useSchemaComponentContext, + useSchemaSettings, + useSchemaSettingsRender, +} from '@nocobase/client'; +import React, { useMemo } from 'react'; + +class PluginHello extends Plugin { + async load() { + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { + const { dn } = useSchemaSettings(); + + return ( + { + dn.shallowMerge({ + 'x-decorator-props': { + title, + }, + }); + }} + /> + ); +}; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'blockTitle', + Component: SchemaSettingsBlockTitleItem, + }, + ], +}); + +const Hello = (props) => { + return ( +

+ Hello, world! + {props.children} +

+ ); +}; + +const Demo = () => { + const fieldSchema = useFieldSchema(); + const field = useField(); + const api = useAPIClient(); + const { refresh } = useSchemaComponentContext(); + const dn = useMemo( + () => + createDesignable({ + current: fieldSchema.parent, + model: field.parent, + api, + refresh, + }), + [], + ); + const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], { + fieldSchema: dn.current, + field: dn.model, + dn, + }); + return ( +
+
{render()}
+
可以进行参数的二次覆盖:{render({ style: { color: 'red' } })}
+
+ ); +}; + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +const app = new Application({ + schemaSettings: [mySettings], + router: { + type: 'memory', + }, + designable: true, + components: { FormItem, Input, Grid, CardItem, Hello }, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/designable.md b/packages/core/client/docs/zh-CN/core/ui-schema/designable.md new file mode 100644 index 0000000000..75b151b260 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/designable.md @@ -0,0 +1,791 @@ +# Designable + +## Designable + +对 Schema 节点进行增、删、改操作,并且提供了事件触发机制,用于将数据同步到服务端。 + +```tsx | pure +interface Options { + current: Schema; + api?: APIClient; + onSuccess?: any; + refresh?: () => void; + t?: any; +} + +interface InsertAdjacentOptions { + wrap?: (s: ISchema) => ISchema; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | BreakFn; + onSuccess?: any; +} + +class Designable { + constructor(options: Options ) { } + loadAPIClientEvents(): void; + on(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', listener: any): void + emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch' | 'batchPatch', ...args: any[]): Promise + refresh(): void + recursiveRemoveIfNoChildren(schema?: Schema, options?: RecursiveRemoveOptions): Schema; + remove(schema?: Schema, options?: RemoveOptions): Promise + removeWithoutEmit(schema?: Schema, options?: RemoveOptions): Schema + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void | Promise + insertBeforeBeginOrAfterEnd(schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeBegin(schema: ISchema, options?: InsertAdjacentOptions): void + insertAfterBegin(schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeEnd(schema: ISchema, options?: InsertAdjacentOptions): Promise + insertAfterEnd(schema: ISchema, options?: InsertAdjacentOptions): void +} +``` + +### 构造函数 + +- 参数讲解 + + - `current`:需要操作的 Schema 节点 + - `api`:用于发起后端请求的 [APIClient](https://docs.nocobase.com/api/sdk) 实例 + - `onSuccess`:后端接口请求成功后的回调 + - `refresh`:用于更新节点后,刷新页面 + - `t`:`useTranslation()` 的返回值 +- 示例 + +```tsx | pure +const schema = new Schema({ + type: 'void', + name: 'hello', + 'x-component': 'div', +}) + +const dn = new Designable({ current: schema }); +``` + +### Schema 操作方法 + +```tsx | pure +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + } + } +}); + +const b = schema.b; + +const dn = createDesignable({ + current: b, +}) + +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +#### remove + +移除当前节点 + +```tsx | pure +dn.remove(); +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.remove(); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { +- b: { +- type: 'void', +- properties: { +- c: { +- type: 'void', +- } +- } +- } + } +} +``` + +#### insertBeforeBegin + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertBeforeBegin({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertBeforeBegin({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { ++ d: { ++ type: 'void', ++ }, + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + } + } +} +``` + +#### insertAfterBegin + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertAfterBegin({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertAfterBegin({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { ++ d: { ++ type: 'void', ++ }, + c: { + type: 'void', + } + } + } + } +} +``` + +#### insertBeforeEnd + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertBeforeEnd({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertBeforeEnd({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, ++ d: { ++ type: 'void', ++ }, + } + } + } +} +``` + +#### insertAfterEnd + +在当前节点的前面插入,并会触发 `insertAdjacent` 事件。 + +```tsx | pure +dn.insertAfterEnd({ + type: 'void', + name: 'd', +}); +console.log(schema.toJSON()); +``` + + +```tsx +import React from 'react'; +import { Schema } from '@formily/json-schema'; +import { createDesignable } from '@nocobase/client'; + +const schema = new Schema({ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + }, + }, + }, + }, +}); + +const b = schema.properties['b']; + +const dn = createDesignable({ + current: b, +}); + +dn.insertAfterEnd({ + type: 'void', + name: 'd', +}); + +export default () =>
{JSON.stringify(schema.toJSON(), null, 2)}
; +``` + +```diff +{ + type: 'void', + name: 'a', + properties: { + b: { + type: 'void', + properties: { + c: { + type: 'void', + } + } + }, ++ d: { ++ type: 'void', ++ }, + } +} +``` + +#### insertAdjacent + +根据第一个参数决定插入的位置,是前面四个方法的封装。 + +```tsx | pure +class Designable { + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void | Promise +} +``` + +### 事件监听和 API 请求 + +- `on` :添加事件监听的基础方法 +- `loadAPIClientEvents`:调用 `on` 方法添加对 `insertAdjacent`、`patch`、`batchPatch`、`remove` 的事件的监听,主要功能是将变更的 Schema 更新到服务端 +- `emit`:是根据事件名称,调用之前注册过的方法,具体是由前面讲过的 *插入操作和删除操作* 触发 + +而 `loadAPIClientEvents()` 并非在初始化时调用,需要手动调用,换而言之,如果不调用 `dn.loadAPIClientEvents()`,则不会将更新发送到服务端,主要是简化在单测或者 DEMO 环境对服务端的 Mock。 + +## 工具函数 + +### createDesignable() + +对 `new Designable()` 的简单封装。 + +```tsx | pure +function createDesignable(options: CreateDesignableProps) { + return new Designable(options); +} +``` + +```tsx | pure +const dn = createDesignable({ current: schema }); +``` + +## Hooks + +### useFieldSchema() + +用户获取当前节点 Schema JSON 对象,更多信息请参考 [formily useFieldSchema()](https://react.formilyjs.org/api/hooks/use-field-schema)。 + +- 类型 + +```tsx | pure +import { Schema } from '@formily/json-schema'; +const useFieldSchema: () => Schema; +``` + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import React from 'react'; +import { useFieldSchema } from '@formily/react'; +import { Application, Plugin, SchemaComponent } from '@nocobase/client'; +const Demo = ({ children }) => { + const fieldSchema = useFieldSchema(); + return
+
{ JSON.stringify(fieldSchema, null, 2)}
+
{children}
+
+} +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Demo', // 这里是 Demo 组件 + 'properties': { + 'world': { + 'type': 'void', + 'x-component': 'Demo', // 这里也是 Demo 组件 + }, + } +} + +const Root = () => { + return +} + +const app = new Application({ + providers: [Root] +}) + +export default app.getRootComponent(); +``` + +### useField() + +获取当前节点 Schema 实例,更多信息请参考 [formily useField()](https://react.formilyjs.org/api/hooks/use-field) + +- 类型 + +```tsx | pure +import { GeneralField } from '@formily/core'; +const useField: () => T; +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const field = useField(); + console.log('field', field); + return
+} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Demo', // 这里是 Demo 组件 + 'properties': { + 'world': { + 'type': 'void', + 'x-component': 'Demo', // 这里也是 Demo 组件 + }, + } +} + +const Root = () => { + return +} +``` + +### useDesignable() + +对当前 Schema 节点的修改操作。 + +- 类型 + +```tsx | pure +interface InsertAdjacentOptions { + wrap?: (s: ISchema) => ISchema; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | BreakFn; + onSuccess?: any; +} + +type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; + +function useDesignable(): { + dn: Designable; + designable: boolean; + reset: () => void; + refresh: () => void; + setDesignable: (value: boolean) => void; + findComponent(component: any): any; + patch: (key: ISchema | string, value?: any) => void + on(name: "error" | "insertAdjacent" | "remove" | "patch" | "batchPatch", listener: any): void + remove(schema?: any, options?: RemoveOptions): void + insertAdjacent(position: Position, schema: ISchema, options?: InsertAdjacentOptions): void + insertBeforeBegin(schema: ISchema): void; + insertAfterBegin(schema: ISchema): void; + insertBeforeEnd(schema: ISchema): void; + insertAfterEnd(schema: ISchema): void; +} +``` + +- 详细解释 + + - designable、reset、refresh、setDesignable:这些值继承自 [SchemaComponentContext](https://www.baidu.com) + - dn:是 `Designable` 的实例 + - findComponent:用于查找 Schema 中字符串对应真正的组件,如果组件未注册则返回 `null` + - remove:内部调用的是 `dn.remove` 方法 + - on:内部调用的是 `dn.on` 方法 + - insertAdjacent:插入新的 Schema 节点,内部调用的是 `dn.insertAdjacent` 方法 + - position:插入位置 + - schema:新的 Schema 节点 + - Options + - wrap:对 Schema 的二次处理的回调函数 + - removeParentsIfNoChildren:当没有子元素时,删除父元素 + - breakRemoveOn:停止删除的判断回调 + - onSuccess:插入成功的回调 + - insertBeforeBegin:内部调用的是 `dn.insertBeforeBegin` 方法 + - insertAfterBegin:内部调用的是 `dn.insertAfterBegin` 方法 + - insertBeforeEnd:内部调用的是 `dn.insertBeforeEnd` 方法 + - insertAfterEnd:内部调用的是 `dn.insertAfterEnd` 方法 +- 示例 + +插入节点。 + +```tsx +import React from 'react'; +import { + SchemaComponentProvider, + SchemaComponent, + useDesignable, +} from '@nocobase/client'; +import { observer, Schema, useFieldSchema } from '@formily/react'; +import { Button, Space } from 'antd'; +import { uid } from '@formily/shared'; + +const Hello = observer( + (props) => { + const { insertAdjacent } = useDesignable(); + const fieldSchema = useFieldSchema(); + return ( +
+

{fieldSchema.name}

+ + + + + + +
{props.children}
+
+ ); + }, + { displayName: 'Hello' }, +); + +const Page = observer( + (props) => { + return
{props.children}
; + }, + { displayName: 'Page' }, +); + +export default () => { + return ( + + + + ); +}; +``` + +部分更新。 + +```tsx +import React from 'react'; +import { + SchemaComponentProvider, + SchemaComponent, + useDesignable, +} from '@nocobase/client'; +import { observer, Schema, useField, useFieldSchema } from '@formily/react'; +import { FormItem } from '@formily/antd-v5'; +import { Button } from 'antd'; +import { uid } from '@formily/shared'; + +const Hello = observer( + (props) => { + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + return ( +
+

{field.title}

+ { JSON.stringify(props) } +
+ { JSON.stringify(field.componentProps) } +
+ { JSON.stringify(field.decoratorProps) } +
+ { JSON.stringify(fieldSchema.toJSON()) } +
+ + +
+ ); + }, + { displayName: 'Hello' }, +); + +const Page = observer( + (props) => { + return
{props.children}
; + }, + { displayName: 'Page' }, +); + +export default () => { + return ( + + + + ); +}; +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-component.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-component.md new file mode 100644 index 0000000000..70c16cfe02 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-component.md @@ -0,0 +1,208 @@ +# SchemaComponent + +## Context + +### SchemaComponentContext + +```tsx | pure +interface SchemaComponentContext { + scope?: any; + components?: SchemaReactComponents; + refresh?: () => void; + reset?: () => void; + designable?: boolean; + setDesignable?: (value: boolean) => void; +} +``` + +Schema 渲染的上下文。 + +- `scope`:Schema 中变量的映射 +- `components`: Schema 中组件的映射 +- `refresh`:触发 React 重新渲染的工具函数 +- `reset`:重置整个 Schema 节点 +- `designable`:是否显示设计器,默认 `false` +- `setDesignable`:用于切换 `designable` 的值 + +## Hooks + +### useSchemaComponentContext() + +用于获取 `SchemaComponentContext` 的值,是 `useContext(SchemaComponentContext)` 的封装。 + +## 组件 + +### SchemaComponentProvider + +其是对 `SchemaComponentContext.Provider` 和 [FormProvider ](https://react.formilyjs.org/api/components/form-provider)的封装,并内置在 `Application` 中,并且会将 `app.components` 和 `app.scopes` 传递过去,所以一般情况下 *不需要关注* 此组件。 + +- props + +```tsx | pure +interface SchemaComponentProviderProps { + designable?: boolean; + form?: Form; + scope?: any; + components?: SchemaReactComponents; +} +``` + +- 详细解释 + - `designable`:`SchemaComponentContext` 中 `designable` 的默认值 + - `form`:NocoBase 的 Schema 能力是基于 formily 的 `FormProvider` 提供的,form 是其参数,默认为 `createForm()` + - `scope`:Schema 中所用到的变量,会通过 `SchemaComponentContext` 进行传递 + - `components`:Schema 中所用到的组件,会通过 `SchemaComponentContext` 进行传递 + +### SchemaComponent + +用于渲染 Schema,此组件必须和 `SchemaComponentProvider` 一起使用,因为 `SchemaComponentProvider` 提供了 [FormProvider](https://react.formilyjs.org/api/components/form-provider) 作为渲染 Schema 的根节点。 + +- Props + +```tsx | pure +type SchemaComponentProps = (ISchemaFieldProps | IRecursionFieldProps) & { + memoized?: boolean; + components?: SchemaReactComponents; + scope?: any; +} +``` + +- 详细解释 + + - `memoized`:当为 `true` 时,会对每层的 Schema 使用 `useMemo()` 进行处理 + - `components`:同 `SchemaComponentProvider` 的 `components` + - `scope`: 同 `SchemaComponentProvider` 的 `components` + +## 综合示例 + +结合 `SchemaComponentProvider`、 `useSchemaComponentContext()` 和 `SchemaComponent`。 + +```tsx +/** + * defaultShowCode: true + */ +import { SchemaComponentProvider, useSchemaComponentContext, SchemaComponent, } from '@nocobase/client'; +const Hello = () => { + const { designable, setDesignable } = useSchemaComponentContext(); + return
+
hello world
+ +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', +} + +const Demo = () => { + return +} + +const Root = () => { + return + + +} + +export default Root; +``` + +使用 `new Application()` 的方式,其内置了 `SchemaComponentProvider` ,我们可以如下操作: + +```tsx +/** + * defaultShowCode: true + */ +import { Application, Plugin, useSchemaComponentContext, SchemaComponent } from '@nocobase/client'; +const Hello = () => { + const { designable, setDesignable } = useSchemaComponentContext(); + return
+
hello world
+ +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', +} + +const HomePage = () => { + return +} + +class MyPlugin extends Plugin { + async load() { + this.app.addComponents({ Hello }); + this.app.router.add('home', { + path: '/', + Component: HomePage, + }) + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}) + +export default app.getRootComponent(); +``` + +### SchemaComponentOptions + +在应用中,会有很多层级的嵌套,每一层都可能提供自己的组件和 scope,此组件就是为了层层传递 Schema 所需的 `components` 和 `scope` 的。 + +- props + +```tsx | pure +interface SchemaComponentOptionsProps { + scope?: any; + components?: SchemaReactComponents; +} +``` + +- 示例 + +```tsx +/** + * defaultShowCode: true + */ +import { SchemaComponentProvider, useSchemaComponentContext, SchemaComponent, SchemaComponentOptions } from '@nocobase/client'; +const World = () => { + return
world
+} + +const Hello = ({ children }) => { + return
+
hello
+ { children } +
; +} + +const schema = { + type: 'void', + name: 'hello', + 'x-component': 'Hello', + properties: { + world: { + type: 'void', + 'x-component': 'World', + }, + }, +} + +const Root = () => { + return + + +} + +export default Root; +``` diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer-manager.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer-manager.md new file mode 100644 index 0000000000..483f9d2b6c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer-manager.md @@ -0,0 +1,186 @@ +# SchemaInitializerManager + +## 实例方法 + +### schemaInitializerManager.add() + +添加 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + add(...schemaInitializerList: SchemaInitializer[]): void +} +``` + +- 示例 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add block', + items: [ + { + name: 'demo', + type: 'item', + title: 'Demo' + } + ], +}); + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + } +} +``` + +### schemaInitializerManager.get() + +获取一个 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + get(name: string): SchemaInitializer | undefined +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + } +} +``` + +### schemaInitializerManager.getAll() + +获取所有的 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + getAll(): Record> +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const list = this.app.schemaInitializerManager.getAll(); + } +} +``` + +### app.schemaInitializerManager.has() + +判断是否有存在某个 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + has(name: string): boolean +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasMyInitializer = this.app.schemaInitializerManager.has('MyInitializer'); + } +} +``` + +### schemaInitializerManager.remove() + +移除 SchemaInitializer 实例。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + remove(name: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.remove('MyInitializer'); + } +} +``` + +### schemaInitializerManager.addItem() + +添加 SchemaInitializer 实例的 Item 项,其和直接 schemaInitializer.add() 方法的区别是,可以确保在实例存在时才会添加。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + addItem(schemaInitializerName: string, itemName: string, data: Omit): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再添加子项,需要确保已注册 + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + if (myInitializer) { + myInitializer.add('b', { type: 'item', title: 'B' }) + } + + // 方式2:通过 addItem,内部确保在 MyInitializer 注册时才会添加 + this.app.schemaInitializerManager.addItem('MyInitializer', 'b', { + type: 'item', + title: 'B' + }) + } +} +``` + +### schemaInitializerManager.removeItem() + +移除 实例的 Item 项,其和直接 schemaInitializer.remove() 方法的区别是,可以确保在实例存在时才会移除。 + +- 类型 + +```tsx | pure +class SchemaInitializerManager { + removeItem(schemaInitializerName: string, itemName: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再删除子项,需要确保已注册 + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + if (myInitializer) { + myInitializer.remove('a') + } + + // 方式2:通过 addItem,内部确保在 MyInitializer 注册时才会移除 + this.app.schemaInitializerManager.remove('MyInitializer', 'a') + } +} +``` diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md new file mode 100644 index 0000000000..2637b0a80b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-initializer.md @@ -0,0 +1,704 @@ +# SchemaInitializer + +## new SchemaInitializer(options) + +```tsx | pure +interface SchemaInitializerOptions { + Component?: ComponentType; + componentProps?: P1; + style?: React.CSSProperties; + title?: string; + icon?: ReactNode; + + items?: SchemaInitializerItemType[]; + ItemsComponent?: ComponentType; + itemsComponentProps?: P2; + itemsComponentStyle?: React.CSSProperties; + + insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; + designable?: boolean; + wrap?: (s: ISchema) => ISchema; + onSuccess?: (data: any) => void; + insert?: InsertType; + useInsert?: () => InsertType; + + popover?: boolean; + popoverProps?: PopoverProps; +} + +class SchemaInitializer { + constructor(options: SchemaInitializerOptions & { name: string }): SchemaInitializer; + add(name: string, item: Omit): void + get(nestedName: string): SchemaInitializerItemType | undefined + remove(nestedName: string): void +} +``` + +### 详细解释 + +![](../static/KTUWb69kioUg8bxYTAMc2ReDnRg.png) + +- name:唯一标识,必填 +- Component 相关 + + - Component:触发组件,默认是 `Button` 组件 + - componentProps: 组件属性,默认是 `ButtonProps` + - title: 按钮的文本 + - icon:按钮的 icon 属性 + - style:组件的样式 +- Items 相关 + + - items:列表项配置 + - ItemsComponent:默认是渲染成一个列表的形式,可通过此参数自定义 items + - itemsComponentProps:`ItemsComponent` 的属性 + - itemsComponentStyle:`ItemsComponent` 的样式 +- popover 组件相关 + + - popover:是否使用 popover,默认为 `true` + - popoverProps:popover 的属性 +- Schema 操作相关 + + - insertPosition:插入位置,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - designable:是否显示设计模式,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - wrap:对 Schema 的二次处理,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - onSuccess:Schema 更新到服务端后的回调,参考:[useDesignable()](/core/ui-schema/designable#usedesignable) + - insert:自定义 Schema 插入逻辑,默认为 [useDesignable()](/core/ui-schema/designable#usedesignable) 的 `insertAdjacent` + - useInsert:当自定义插入 Schema 的逻辑需要用到 Hooks 时,可以使用此参数 + +### 示例 + +#### 基础用法 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + ], +}); +``` + + + + +#### 定制化 `Component` + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + Component: (props) => ( + + C + + ), + componentProps: { + size: 'large', + }, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + } + ], +}); +``` + + + +#### 不使用 Popover + +关于 `useDesignable()` 的说明请参考 [useDesignable](/core/ui-schema/designable#usedesignable)。 + +```tsx | pure +const schema = { + type: 'void', + title: Math.random(), + 'x-component': 'Hello', +}; +const MyInitializerComponent = () => { + const { insertBeforeEnd } = useDesignable(); + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add block', + popover: false, + Component: MyInitializerComponent, +}); +``` + + + + +#### 定制化 Items + +```tsx | pure +const CustomListGridMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ( + + + + )} + > + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + ItemsComponent: CustomListGridMenu, + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + } + ], +}); +``` + + + +## options.items 配置详解 + +### 类型 + +```tsx | pure +interface SchemaInitializerComponentCommonProps { + title?: string; + schema?: ISchema; + style?: React.CSSProperties; + className?: string; +} + +interface SchemaInitializerItemBaseType extends SchemaInitializerComponentCommonProps { + name: string; + sort?: number; + type?: string; + Component?: string | ComponentType; + componentProps?: Omit; + useComponentProps?: () => Omit; + useVisible?: () => boolean; + children?: SchemaInitializerItemType[]; + useChildren?: () => SchemaInitializerItemType[]; + [index: string]: any; +} +``` + +### 两种定义方式:`Component` 和 `type` + + +- 通过 `Component` 定义 + +```tsx | pure + +const Demo = () => { + // 最终渲染 `SchemaInitializerItem` + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + } + ], +}); +``` + +- 通过 `type` 定义 + +NocoBase 内置了一些常用的 `type`,例如 `type: 'item'`,相当于 `Component: SchemaInitializerItem`。 + +更多内置类型,请参考:[内置组件和类型](/core/ui-schema/schema-initializer#%E5%86%85%E7%BD%AE%E7%BB%84%E4%BB%B6%E5%92%8C%E7%B1%BB%E5%9E%8B) + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + items: [ + { + name: 'a', + type: 'item', + title: 'Demo' + } + ], +}); +``` + + + +### `children` 和动态方式 `useChildren` + +对于某些组件而言是有子列表项的,例如 `type: 'itemGroup'`,那么我们使用 children 属性,同时考虑到某些场景下 children 是动态的,需要从 Hooks 里面获取,那么就可以通过 `useChildren` 来定义。 + + + +### 动态显示隐藏 `useVisible` + + + +### 组件属性 `componentProps` 和动态属性 `useComponentProps` + +对于一些通用组件,我们可以通过 `componentProps` 来定义组件属性,同时考虑到某些场景下组件属性是动态的,需要从 Hooks 里面获取,那么就可以通过 `useComponentProps` 来定义。 + +当然也可以不使用这两个属性,直接封装成一个组件,然后通过 `Component` 来定义。 + + + +### 公共属性和组件属性 + +```tsx | pure +{ + name: 'demo', + title: 'Demo', + foo: 'bar', + Component: Demo, + componentProps: { + zzz: 'xxx', + }, +} +``` + +从上面的示例中我么看到,从配置项中获取组件组件所需的数据有两个方式: + +- 组件属性:通过 `componentProps` 来定义,例如 `zzz: 'xxx'` +- 公共属性:将属性直接定义在配置项上,例如 `foo: 'bar'`、`name`、`title` + +在获取上 + +- `componentProps` 定义的数据会被传递给组件的 `props` +- 直接定义在配置项上的数据会则需要通过 [useSchemaInitializerItem()](/core/ui-schema/schema-initializer#useschemainitializeritem) 获取 + +```tsx | pure +const Demo = (props) => { + console.log(props); // { zzz: 'xxx' } + const { foo } = useSchemaInitializerItem(); // { foo: 'bar' } +} +``` + +## 实例方法 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + } + ], + }, + ], +}); +``` + +```tsx +import { SchemaInitializer, Application, useSchemaInitializerRender } from '@nocobase/client'; +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + type: 'item', + } + ], + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return render(); +} +const app = new Application({ + schemaInitializers: [myInitializer], + providers: [Root], + designable: true, +}); + +export default app.getRootComponent(); +``` + +### schemaInitializer.add() + +用于新增 Item,另一种添加方式参考 [schemaInitializerManager.addItem()](/core/ui-schema/schema-initializer-manager#schemainitializermanageradditem); + +- 类型 + +```tsx | pure +class SchemaInitializer { + add(name: string, item: Omit): void +} +``` + +- 参数说明 + +第一个参数是 name,作为唯一标识,用于增删改查,并且 `name` 支持 `.` 用于分割层级。 + +- 示例 + +```tsx | pure +myInitializer.add('b', { + type: 'item', + title: 'item b', +}) + +myInitializer.add('a.a2', { + type: 'item', + title: 'item a2', +}) +``` + +```tsx +import { SchemaInitializer, Application, useSchemaInitializerRender } from '@nocobase/client'; +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'item a', + type: 'itemGroup', + children: [ + { + name: 'a1', + title: 'item a1', + type: 'item', + } + ], + }, + ], +}); + +myInitializer.add('b', { + type: 'item', + title: 'item b', +}) + +myInitializer.add('a.a2', { + type: 'item', + title: 'item a2', +}) + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return render(); +} +const app = new Application({ + schemaInitializers: [myInitializer], + providers: [Root], + designable: true, +}); + + +export default app.getRootComponent(); +``` + + +### schemaInitializer.get() + +- 类型 + +```tsx | pure +class SchemaInitializer { + get(nestedName: string): SchemaInitializerItemType | undefined +} +``` + +- 示例 + +```tsx | pure +const itemA = myInitializer.get('a') + +const itemA1 = myInitializer.add('a.a1') +``` + +### schemaInitializer.remove() + +另一种移除方式参考 [schemaInitializerManager.addItem()](/core/ui-schema/schema-initializer-manager#schemainitializermanagerremoveitem); + +- 类型 + +```tsx | pure +class SchemaInitializer { + remove(nestedName: string): void +} +``` + +- 示例 + +```tsx | pure +myInitializer.remove('a.a1') + +myInitializer.remove('a') +``` + +## Hooks + +### useSchemaInitializer() + +用于获取 `SchemaInitializer` 上下文内容。 + +- 类型 + +```tsx | pure +export type InsertType = (s: ISchema) => void; + +const useSchemaInitializer: () => { + insert: InsertType; + options: SchemaInitializerOptions; + visible?: boolean; + setVisible?: (v: boolean) => void; +} +``` + +- 参数详解 + - `insert`:参数是 Schema 对象,用于插入 Schema + - `options`:获取 `new SchemaInitializer(options)` 时 options 配置 + - `visible`:popover 是否显示 + - `setVisible`:设置 popover 显示状态 + +- 示例 + +```tsx | pure +const schema = { + type: 'void', + 'x-component': 'Hello', +} +const Demo = () => { + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert(schema); + }; + return ; +} +``` + +### useSchemaInitializerRender() + +用于渲染 `SchemaInitializer`。 + +- 类型 + +```tsx | pure +function useSchemaInitializerRender(name: string, options?: SchemaInitializerOptions): { + exists: boolean; + render: (props?: SchemaInitializerOptions) => React.FunctionComponentElement; +} +``` + +- 参数详解 + +返回的 `render` 方法可以接收一个参数,用于覆盖 `new SchemaInitializer(options)` 的 `options` 配置。 + +- 示例 + +```tsx | pure +const Demo = () => { + const filedSchema = useFieldSchema(); + const { render } = useSchemaInitializerRender(fieldSchema['x-initializer'], fieldSchema['x-initializer-props']); + return
+
{ render() }
+
可以进行参数的二次覆盖:{ render({ style: { color: 'red' } }) }
+
+} +``` + + + +### useSchemaInitializerItem() + +用于获取配置项内容的,配置项是指的 `SchemaInitializer` 中的 `items` 中的一项。 + +- 类型 + +```tsx | pure +const useSchemaInitializerItem: () => T +``` + +- 示例 + +```tsx | pure +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'a', + title: 'Item A', + foo: 'bar', + Component: Demo, + }, + ], +}); + +/** + * 通过 useSchemaInitializerItem() 获取到的是 + * { + * name: 'a', + * title: 'Item A', + * foo: 'bar', + * Component: Demo, + * } + */ +const Demo = () => { + const { title, foo } = useSchemaInitializerItem(); + return
{ title } - { foo }
+} +``` + + + +## 内置组件和类型 + +| type | Component | 效果 | +| ----------- | ------------------------------ | ----------------------------------------- | +| item | SchemaInitializerItem | 文本| +| itemGroup | SchemaInitializerItemGroup | 分组,同 antd `Menu` 组件的 `type: 'group'` | +| subMenu | SchemaInitializerSubMenu | 子菜单,同 antd `Menu` 组件的子菜单 | +| divider | SchemaInitializerDivider | 分割线,同 antd `Menu` 组件的 `type: 'divider'` | +| switch | SchemaInitializerSwitch | 开关 | +| actionModal | SchemaInitializerActionModal | 弹窗| + +以下每个示例都提供了 2 种[定义方式](/core/ui-schema/schema-initializer#两种定义方式component-和-type),一种是通过 `Component` 定义,另一种是通过 `type` 定义。 + +### `type: 'item'` & `SchemaInitializerItem` + +文本项。 + +```tsx | pure +interface SchemaInitializerItemProps { + style?: React.CSSProperties; + className?: string; + name?: string; + icon?: React.ReactNode; + title?: React.ReactNode; + items?: SchemaInitializerItemType[]; + onClick?: (args?: any) => any; +} +``` + +核心参数是 `title`、`icon`、`onClick`、`items`,其中 `onClick` 用于插入 Schema,`items` 用于渲染子列表项。 + + + +### `type: 'itemGroup'` & SchemaInitializerItemGroup + +分组。 + +```tsx | pure +interface SchemaInitializerItemGroupProps { + name: string; + title: string; + children?: SchemaInitializerOptions['items']; + divider?: boolean; +} +``` + +核心参数是 `title`、`children`,其中 `children` 用于渲染子列表项,`divider` 用于渲染分割线。 + + + +### `type: 'switch'` & SchemaInitializerSwitch + +Switch 切换按钮。 + +```tsx | pure +interface SchemaInitializerSwitchItemProps extends SchemaInitializerItemProps { + checked?: boolean; + disabled?: boolean; +} +``` + +核心参数是 `checked`、`onClick`,其中 `onClick` 用于插入或者移除 Schema。 + + + +### `type: 'subMenu'` & SchemaInitializerSubMenu + +子菜单。 + + + +### `type: 'divider'` & SchemaInitializerDivider + +分割线。 + + + +## 渲染组件 + +### SchemaInitializerChildren + +用于自定义渲染多个列表项。 + +```tsx | pure + +const Demo = ({ children }) => { + // children: [{ name: 'a1', Component: ItemA1 }, { name: 'a2', type: 'item', title: 'ItemA2' }] + return { children } +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + items: [ + { + name: 'test', + Component: Demo, + children: [ + { + name: 'a1', + Component: ItemA1, + }, + { + name: 'a2', + type: 'item', + title: 'ItemA2', + } + ] + } + ], +}); +``` + +### SchemaInitializerChild + +用于自定义渲染单个列表项。 + +```tsx | pure +const Demo = (props) => { + return +} +``` diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings-manager.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings-manager.md new file mode 100644 index 0000000000..e4f43a33d9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings-manager.md @@ -0,0 +1,188 @@ +# SchemaSettingsManager + +## 实例方法 + +### schemaSettingsManager.add() + +添加 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + add(...schemaSettingList: SchemaSetting[]): void +} +``` + +- 示例 + +```tsx | pure +const mySchemaSettings = new SchemaSetting({ + name: 'MySchemaSettings', + title: 'Add block', + items: [ + { + name: 'demo', + type: 'item', + componentProps:{ + title: 'Demo' + } + } + ], +}); + +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.add(mySchemaSettings); + } +} +``` + +### schemaSettingsManager.get() + +获取一个 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + get(name: string): SchemaSetting | undefined +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.getAll() + +获取所有的 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + getAll(): Record> +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const list = this.app.schemaSettingsManager.getAll(); + } +} +``` + +### app.schemaSettingsManager.has() + +判断是否有存在某个 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + has(name: string): boolean +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + const hasMySchemaSettings = this.app.schemaSettingsManager.has('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.remove() + +移除 SchemaSettings 实例。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + remove(name: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.remove('MySchemaSettings'); + } +} +``` + +### schemaSettingsManager.addItem() + +添加 SchemaSettings 实例的 Item 项,其和直接 schemaInitializer.add() 方法的区别是,可以确保在实例存在时才会添加。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + addItem(schemaInitializerName: string, itemName: string, data: Omit): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再添加子项,需要确保已注册 + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + if (mySchemaSettings) { + mySchemaSettings.add('b', { type: 'item', componentProps:{ title: 'B' } }) + } + + // 方式2:通过 addItem,内部确保在 mySchemaSettings 注册时才会添加 + this.app.schemaSettingsManager.addItem('MySchemaSettings', 'b', { + type: 'item', + componentProps:{ title: 'B' } + }) + } +} +``` + +### schemaSettingsManager.removeItem() + +移除 实例的 Item 项,其和直接 schemaInitializer.remove() 方法的区别是,可以确保在实例存在时才会移除。 + +- 类型 + +```tsx | pure +class SchemaSettingsManager { + removeItem(schemaInitializerName: string, itemName: string): void +} +``` + +- 示例 + +```tsx | pure +class MyPlugin extends Plugin { + async load() { + // 方式1:先获取,再删除子项,需要确保已注册 + const mySchemaSettings = this.app.schemaSettingsManager.get('MySchemaSettings'); + if (mySchemaSettings) { + mySchemaSettings.remove('a') + } + + // 方式2:通过 addItem,内部确保在 mySchemaSettings 注册时才会移除 + this.app.schemaSettingsManager.remove('MySchemaSettings', 'a') + } +} +``` diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings.md new file mode 100644 index 0000000000..466a131cb2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-settings.md @@ -0,0 +1,467 @@ +# SchemaSettings + +## new SchemaSettings(options) + +创建一个 SchemaSettings 实例。 + +```tsx | pure +interface SchemaSettingsOptions { + name: string; + Component?: ComponentType; + componentProps?: T; + style?: React.CSSProperties; + + items: SchemaSettingsItemType[]; +} + +class SchemaSettings{ + constructor(options: SchemaSettingsOptions): SchemaSettings; + add(name: string, item: Omit): void + get(nestedName: string): SchemaSettingsItemType | undefined + remove(nestedName: string): void +} +``` + +### 详细解释 + +![](../static/VfPGbhWo9os0qTxcITkcHNfin4g.png) + +- name:唯一标识,必填 +- Component 相关 + + - Component:触发组件,默认是 `` 组件 + - componentProps: 组件属性 + - style:组件的样式 +- items:列表项配置 + +### 示例 + +#### 基础用法 + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + items: [ + { + name: 'demo1', // 唯一标识 + type: 'item', // 内置类型 + componentProps: { + title: 'DEMO1', + onClick() { + alert('DEMO1'); + }, + }, + }, + { + name: 'demo2', + Component: () => alert('DEMO2')} />, // 直接使用 Component 组件 + }, + ], +}); +``` + +#### 定制化 `Component` + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + Component: Button, // 自定义组件 + componentProps: { + type: 'primary', + children: '自定义按钮', + }, + // Component: (props) => , // 等同于上面效果 + items: [ + { + name: 'demo1', + type: 'item', + componentProps: { + title: 'DEMO', + }, + }, + ], +}); +``` + +## options.items 配置详解 + +```tsx | pure +interface SchemaSettingsItemCommon { + name: string; + sort?: number; + type?: string; + Component: string | ComponentType; + useVisible?: () => boolean; + children?: SchemaSettingsItemType[]; + useChildren?: () => SchemaSettingsItemType[]; + checkChildrenLength?: boolean; + componentProps?: Omit; + useComponentProps?: () => Omit; +} +``` + +### 两种定义方式:`Component` 和 `type` + + +- 通过 `Component` 定义 + +```tsx | pure + +const Demo = () => { + // 最终渲染 `SchemaSettingsItem` + return +} + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + Component: Demo, // 通过 Component 定义 + } + ], +}); +``` + +- 通过 `type` 定义 + +NocoBase 内置了一些常用的 `type`,例如 `type: 'item'`,相当于 `Component: SchemaSettingsItem`。 + +更多内置类型,请参考:[内置组件和类型](/core/ui-schema/schema-settings#%E5%86%85%E7%BD%AE%E7%BB%84%E4%BB%B6%E5%92%8C%E7%B1%BB%E5%9E%8B) + +```tsx | pure +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'a', + type: 'item', + componentProps: { + title: 'Demo', + }, + } + ], +}); +``` + + + +### `children` 和动态方式 `useChildren` + +对于某些组件而言是有子列表项的,例如 `type: 'itemGroup'`,那么我们使用 children 属性,同时考虑到某些场景下 children 是动态的,需要从 Hooks 里面获取,那么就可以通过 `useChildren` 来定义。 + + + +### 动态显示隐藏 `useVisible` + + + +### 组件属性 `componentProps` 和动态属性 `useComponentProps` + +对于一些通用组件,我们可以通过 `componentProps` 来定义组件属性,同时考虑到某些场景下组件属性是动态的,需要从 Hooks 里面获取,那么就可以通过 `useComponentProps` 来定义。 + +当然也可以不使用这两个属性,直接封装成一个组件,然后通过 `Component` 来定义。 + + + +## 实例方法 + +```tsx | pure +const mySchemaSettings = new SchemaSettings({ + name: 'MySchemaSettings', + items: [ + { + name: 'a', + type: 'itemGroup', + componentProps: { + title: 'item a' + }, + children: [ + { + name: 'a1', + title: 'item a1', + } + ], + }, + ], +}); +``` + +### schemaSettings.add() + +用于新增 Item。 + +- 类型 + +```tsx | pure +class SchemaSettings { + add(name: string, item: Omit): void +} +``` + +- 参数说明 + +第一个参数是 name,作为唯一标识,用于增删改查,并且 `name` 支持 `.` 用于分割层级。 + +- 示例 + +```tsx | pure +mySchemaSetting.add('b', { + type: 'item', + title: 'item b', +}) + +mySchemaSetting.add('a.a2', { + type: 'item', + title: 'item a2', +}) +``` + +### schemaSettings.get() + +- 类型 + +```tsx | pure +class SchemaSettings { + get(nestedName: string): SchemaSettingsItemType| undefined +} +``` + +- 示例 + +```tsx | pure +const itemA = mySchemaSetting.get('a') + +const itemA1 = mySchemaSetting.add('a.a1') +``` + +### schemaSettings.remove() + +- 类型 + +```tsx | pure +class SchemaSettings { + remove(nestedName: string): void +} +``` + +- 示例 + +```tsx | pure +mySchemaSetting.remove('a.a1') + +mySchemaSetting.remove('a') +``` + +## Hooks + +### useSchemaSettingsRender() + +用于渲染 SchemaInitializer。 + +- 类型 + +```tsx | pure +function useSchemaSettingsRender(name: string, options?: SchemaSettingsOptions): { + exists: boolean; + render: (options?: SchemaSettingsRenderOptions) => React.ReactElement; +} +``` + +- 示例 + +```tsx | pure +const Demo = () => { + const filedSchema = useFieldSchema(); + const { render, exists } = useSchemaSettingsRender(fieldSchema['x-settings'], fieldSchema['x-settings-props']) + return
+
{ render() }
+
可以进行参数的二次覆盖:{ render({ style: { color: 'red' } }) }
+
+} +``` + + + +### useSchemaSettings() + +获取 schemaSetting 上下文数据。 + +上下文数据包含了 `schemaSetting` 实例化时的 `options` 以及调用 `useSchemaSettingsRender()` 时传入的 `options`。 + +- 类型 + +```tsx | pure +interface UseSchemaSettingsResult extends SchemaSettingsOptions { + dn?: Designable; + field?: GeneralField; + fieldSchema?: Schema; +} + +function useSchemaSettings(): UseSchemaSettingsResult; +``` + +- 示例 + +```tsx | pure +const { dn } = useSchemaSettings(); +``` + +### useSchemaSettingsItem() + +用于获取一个 item 的数据。 + +- 类型 + +```tsx | pure +export type SchemaSettingsItemType = { + name: string; + type?: string; + sort?: number; + Component?: string | ComponentType; + componentProps?: T; + useComponentProps?: () => T; + useVisible?: () => boolean; + children?: SchemaSettingsItemType[]; + [index]: any; +}; + +function useSchemaSettingsItem(): SchemaSettingsItemType; +``` + +- 示例 + +```tsx | pure +const { name } = useSchemaSettingsItem(); +``` + +## 内置组件和类型 + +| type | Component | 效果 | +| ----------- | ------------------------------ | ----------------------------------------- | +| item | SchemaSettingsItem | 文本 | +| itemGroup | SchemaSettingsItemGroup | 分组,同 Menu 组件的 `type: 'itemGroup'` | +| subMenu | SchemaSettingsSubMenu | 子菜单,同 Menu 组件的子菜单 | +| divider | SchemaSettingsDivider | 分割线,同 Menu 组件的 `type: 'divider'` | +| remove | SchemaSettingsRemove | 删除,用于删除一个区块 | +| select | SchemaSettingsSelectItem | 下拉选择 | +| cascader | SchemaSettingsCascaderItem | 级联选择 | +| switch | SchemaSettingsSwitchItem | 开关 | +| popup | SchemaSettingsPopupItem | 弹出层 | +| actionModal | SchemaSettingsActionModalItem | 操作弹窗 | +| modal | SchemaSettingsModalItem | 弹窗 | + +### SchemaSettingsItem + +文本,对应的 `type` 为 `item`。 + +```tsx | pure +interface SchemaSettingsItemProps extends Omit { + title: string; +} +``` + +核心参数为 `title` 和 `onClick`,可以在 `onClick` 中修改 schema。 + + + +### SchemaSettingsItemGroup + +分组,对应的 `type` 为 `itemGroup`。 + +核心参数是 `title`。 + + + +### SchemaSettingsSubMenu + +子菜单,对应的 `type` 为 `subMenu`。 + +核心参数是 `title`。 + + + +### SchemaSettingsDivider + +分割线,对应的 `type` 为 `divider`。 + + + +### SchemaSettingsRemove + +删除,对应的 `type` 为 `remove`。 + +```tsx | pure +interface SchemaSettingsRemoveProps { + confirm?: ModalFuncProps; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | ((s: ISchema) => boolean); +} +``` + +- `confirm`:删除前的确认弹窗 +- `removeParentsIfNoChildren`:如果删除后没有子节点了,是否删除父节点 +- `breakRemoveOn`:如果删除的节点满足条件,是否中断删除 + + + +### SchemaSettingsSelectItem + +选择器,对应的 `type` 为 `select`。 + + + +### SchemaSettingsCascaderItem + +级联选择,对应的 `type` 为 `cascader`。 + +### SchemaSettingsSwitchItem + +开关,对应的 `type` 为 `switch`。 + + + +### SchemaSettingsModalItem + +弹窗,对应的 `type` 为 `modal`。 + +```tsx | pure +export interface SchemaSettingsModalItemProps { + title: string; + onSubmit: (values: any) => void; + initialValues?: any; + schema?: ISchema | (() => ISchema); + modalTip?: string; + components?: any; + hidden?: boolean; + scope?: any; + effects?: any; + width?: string | number; + children?: ReactNode; + asyncGetInitialValues?: () => Promise; + eventKey?: string; + hide?: boolean; +} +``` + +我们可以通过 `schema` 参数来定义弹窗的表单,然后在 `onSubmit` 中获取表单的值,然后修改当前 schema 节点。 + + + +### SchemaSettingsActionModalItem + +操作弹窗,对应的 `type` 为 `actionModal`。 + +其和 `modal` 的区别是,`SchemaSettingsModalItem` 弹窗会丢失上下文,而 `SchemaSettingsActionModalItem` 会保留上下文,简单场景下可以使用 `SchemaSettingsModalItem`,复杂场景下可以使用 `SchemaSettingsActionModalItem`。 + +```tsx | pure +export interface SchemaSettingsActionModalItemProps extends SchemaSettingsModalItemProps, Omit { + uid?: string; + initialSchema?: ISchema; + schema?: ISchema; + beforeOpen?: () => void; + maskClosable?: boolean; +} +``` + + diff --git a/packages/core/client/docs/zh-CN/core/ui-schema/schema-toolbar.md b/packages/core/client/docs/zh-CN/core/ui-schema/schema-toolbar.md new file mode 100644 index 0000000000..dfd809bba0 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/ui-schema/schema-toolbar.md @@ -0,0 +1,427 @@ +# SchemaToolbar + +![SchemaToolbar](../static/designer.png) + +## 组件 + +### SchemaToolbar 组件 + +此为默认的 Toolbar 组件,其内部会自动根据 Schema 的 `x-settings`、`x-initializer` 渲染 `SchemaSettings`、`SchemaInitializer` 和 `Drag` 组件。 + +Toolbar 具体的渲染规则为:当有 `x-toolbar` 会渲染对应的组件;当无 `x-toolbar` 但是有 `x-settings`、`x-initializer` 会渲染默认的 `SchemaToolbar` 组件。 + +- 类型 + +```tsx | pure +interface SchemaToolbarProps { + title?: string; + draggable?: boolean; + initializer?: string | false; + settings?: string | false; + /** + * @default true + */ + showBorder?: boolean; + showBackground?: boolean; +} +``` + +- 详细解释 + - `title`:左上角的标题 + - `draggable`:是否可以拖拽,默认为 `true` + - `initializer`:`SchemaInitializer` 的默认值,当 schema 里没有 `x-initializer` 时,会使用此值;当为 `false` 时,不会渲染 `SchemaInitializer` + - `settings`:`SchemaSettings` 的默认值,当 schema 里没有 `x-settings` 时,会使用此值;当为 `false` 时,不会渲染 `SchemaSettings` + - `showBorder`:边框是否变为橘色 + - `showBackground`:背景是否变为橘色 + +- 示例 + +未指定 `x-toolbar` 时会渲染默认的 `SchemaToolbar` 这个组件。 + +```tsx +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 仅指定了 `x-settings` 但是没有 `x-toolbar`,会使用默认的 `SchemaToolbar` 组件 + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` + +自定义 Toolbar 组件。 + + +```tsx +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const MyToolbar = () => { + return +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello, MyToolbar }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` + +## Hooks + +### useSchemaToolbarRender() + +用于渲染 `SchemaToolbar`。 + +- 类型 + +```tsx | pure +const useSchemaToolbarRender: (fieldSchema: ISchema) => { + render(props?: SchemaToolbarProps): React.JSX.Element; + exists: boolean; +} +``` + +- 详细解释 + +前面示例中 `'x-decorator': 'CardItem'` 中组件 `CardItem` 里面就调用了 `useSchemaToolbarRender()` 进行渲染。内置的组件还有:`BlockItem`、`CardItem`、`Action`、`FormItem`。 + +`render()` 支持二次覆盖组件属性。 + +- 示例 + +```tsx | pure +const MyDecorator = () => { + const filedSchema = useFieldSchema(); + const { render } = useSchemaToolbarRender(filedSchema); // 从 Schema 中读取 Toolbar 组件 + + return { render() } +} +``` + + +```tsx +import { Card } from 'antd'; +import { useFieldSchema } from '@formily/react'; +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + SchemaToolbar, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaToolbarRender +} from '@nocobase/client'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const MyToolbar = (props) => { + return +} + +// 自定义包装器 +const MyDecorator = ({children}) => { + const filedSchema = useFieldSchema(); + // 使用 `useSchemaToolbarRender()` 获取并渲染内容 + const { render } = useSchemaToolbarRender(filedSchema); + return { render({ draggable: false }) }{children} +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Button Text', + wrap: Grid.wrap, + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'MyDecorator', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () => { + const schema = useFieldSchema(); + return

Hello, world! {schema.name}

; +}; + +const hello1 = Grid.wrap({ + type: 'void', + // 使用自定义的 Toolbar 组件 + 'x-toolbar': 'MyToolbar', + 'x-settings': 'mySettings', + 'x-decorator': 'MyDecorator', + 'x-component': 'Hello', +}); + +const HelloPage = () => { + return ( +
+ +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello, MyToolbar, MyDecorator }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); +``` diff --git a/packages/core/client/docs/zh-CN/core/utils.md b/packages/core/client/docs/zh-CN/core/utils.md new file mode 100644 index 0000000000..711c8ad0a8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/utils.md @@ -0,0 +1,7 @@ +# Utils + +## Function + +### tval + +用于输出多语言的字符串。 diff --git a/packages/core/client/docs/zh-CN/index.md b/packages/core/client/docs/zh-CN/index.md new file mode 100644 index 0000000000..fe2d08520b --- /dev/null +++ b/packages/core/client/docs/zh-CN/index.md @@ -0,0 +1,3 @@ +# Index + + diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/add-new.md b/packages/core/client/docs/zh-CN/ui-schema/actions/add-new.md new file mode 100644 index 0000000000..308dab604c --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/add-new.md @@ -0,0 +1,82 @@ +# Add new + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-action": "create", + "x-acl-action": "create", + "title": "{{t('Add new')}}", + "x-designer": "Action.Designer", + "x-component": "Action", + "x-decorator": "ACLActionProvider", + "x-component-props": { + "openMode": "drawer", + "type": "primary", + "component": "CreateRecordAction", + "icon": "PlusOutlined" + }, + "x-align": "right", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Add record\") }}", + "x-component": "Action.Container", + "x-component-props": { + "className": "nb-action-popup" + }, + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializersForCreateFormBlock", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Add new\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "CreateFormBlockInitializers", + "x-uid": "psh2rj6pkab", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "3i7grk0aytf", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "75s24n1nlkh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "wg1f4xyx7vp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "2hwye7mt2th", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/add-record.md b/packages/core/client/docs/zh-CN/ui-schema/actions/add-record.md new file mode 100644 index 0000000000..7dbce5eda0 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/add-record.md @@ -0,0 +1 @@ +# Add record(任意表) \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-edit.md b/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-edit.md new file mode 100644 index 0000000000..b602f30ff3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-edit.md @@ -0,0 +1 @@ +# 批量编辑 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-update.md b/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-update.md new file mode 100644 index 0000000000..9ac6d1890a --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/bulk-update.md @@ -0,0 +1 @@ +# 批量更新 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/custom-request.md b/packages/core/client/docs/zh-CN/ui-schema/actions/custom-request.md new file mode 100644 index 0000000000..b69407f624 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/custom-request.md @@ -0,0 +1,88 @@ +# 自定义请求 + + +x-component 不统一,x-action 也不统一,有三种风格的 schema + + +## UI Schema + +### 表单的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "CustomRequestAction", + "x-action": "customize:form:request", + "x-designer": "CustomRequestAction.Designer", + "x-decorator": "CustomRequestAction.Decorator", + "x-action-settings": { + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "type": "void", + "x-uid": "d1rfalvivfo", + "x-async": false, + "x-index": 2 +} +``` + +### 表格列操作的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "Action.Link", + "x-action": "customize:table:request", + "x-designer": "Action.Designer", + "x-action-settings": { + "requestSettings": {}, + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "x-component-props": { + "useProps": "{{ useCustomizeRequestActionProps }}" + }, + "x-designer-props": { + "linkageAction": true + }, + "type": "void", + "x-uid": "edn0y4fq7xb", + "x-async": false, + "x-index": 1 +} +``` + +### 详情的自定义请求 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Custom request\") }}", + "x-component": "CustomRequestAction", + "x-action": "customize:form:request", + "x-designer": "CustomRequestAction.Designer", + "x-decorator": "CustomRequestAction.Decorator", + "x-action-settings": { + "onSuccess": { + "manualClose": false, + "redirecting": false, + "successMessage": "{{t(\"Request success\")}}" + } + }, + "type": "void", + "x-uid": "glc5zkr0ke3", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/delete.md b/packages/core/client/docs/zh-CN/ui-schema/actions/delete.md new file mode 100644 index 0000000000..d4f3659f0c --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/delete.md @@ -0,0 +1 @@ +# 删除 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/duplicate.md b/packages/core/client/docs/zh-CN/ui-schema/actions/duplicate.md new file mode 100644 index 0000000000..5fc15c69d2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/duplicate.md @@ -0,0 +1 @@ +# 复制 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/edit.md b/packages/core/client/docs/zh-CN/ui-schema/actions/edit.md new file mode 100644 index 0000000000..3092287aa7 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/edit.md @@ -0,0 +1 @@ +# 编辑 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/export.md b/packages/core/client/docs/zh-CN/ui-schema/actions/export.md new file mode 100644 index 0000000000..9e6bd4bd6b --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/export.md @@ -0,0 +1 @@ +# 导出 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/filter.md b/packages/core/client/docs/zh-CN/ui-schema/actions/filter.md new file mode 100644 index 0000000000..653fc6d266 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/filter.md @@ -0,0 +1 @@ +# 筛选 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/import.md b/packages/core/client/docs/zh-CN/ui-schema/actions/import.md new file mode 100644 index 0000000000..a371bffd35 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/import.md @@ -0,0 +1 @@ +# 导入 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/index.md b/packages/core/client/docs/zh-CN/ui-schema/actions/index.md new file mode 100644 index 0000000000..e177708c44 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/index.md @@ -0,0 +1,51 @@ +# 概览 + +## UI Schema 协议 + +- `x-action` +- `x-align` +- `x-acl-action` +- `x-acl-action-props` +- `x-action-settings` + +## 常规操作 + +```json +{ + "type": "void", + "title": "{{ t(\"Submit\") }}", + "x-component": "Action", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, +} +``` + +## 弹窗操作 + +```json +{ + "type": "void", + "title": "{{t('Open drawer')}}", + "x-component": "Action", + "x-component-props": { + "openMode": "drawer", + "type": "primary", + "icon": "PlusOutlined" + }, + "x-align": "right", + "properties": { + "drawer": { + "type": "void", + "x-component": "Action.Container", + "x-component-props": {}, + "properties": { + }, + } + } +} +``` + +更多示例查看 [Action](/apis/action) \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/print.md b/packages/core/client/docs/zh-CN/ui-schema/actions/print.md new file mode 100644 index 0000000000..2823a7a182 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/print.md @@ -0,0 +1 @@ +# 打印 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/refresh.md b/packages/core/client/docs/zh-CN/ui-schema/actions/refresh.md new file mode 100644 index 0000000000..654d97cca9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/refresh.md @@ -0,0 +1 @@ +# 刷新 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/save-record.md b/packages/core/client/docs/zh-CN/ui-schema/actions/save-record.md new file mode 100644 index 0000000000..83d1a5a372 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/save-record.md @@ -0,0 +1 @@ +# 保存数据 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/submit-to-workflow.md b/packages/core/client/docs/zh-CN/ui-schema/actions/submit-to-workflow.md new file mode 100644 index 0000000000..97bfe3b191 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/submit-to-workflow.md @@ -0,0 +1 @@ +# 提交至工作流 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/submit.md b/packages/core/client/docs/zh-CN/ui-schema/actions/submit.md new file mode 100644 index 0000000000..0634eb49d2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/submit.md @@ -0,0 +1,28 @@ +# 提交 + +## UI Schema + +### 新建数据的提交 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Submit\") }}", + "x-action": "submit", + "x-component": "Action", + "x-designer": "Action.Designer", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, + "x-action-settings": { + "triggerWorkflows": [] + }, + "type": "void", + "x-uid": "aaxbm1i5xxd", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/update-record.md b/packages/core/client/docs/zh-CN/ui-schema/actions/update-record.md new file mode 100644 index 0000000000..aa3ff7ffb3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/update-record.md @@ -0,0 +1 @@ +# 更新数据 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/actions/view.md b/packages/core/client/docs/zh-CN/ui-schema/actions/view.md new file mode 100644 index 0000000000..f5fd55d6b8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/actions/view.md @@ -0,0 +1 @@ +# 查看 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/calendar.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/calendar.md new file mode 100644 index 0000000000..13e6c108e6 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/calendar.md @@ -0,0 +1,282 @@ +# Calendar + +## x-initializer + +- CalendarActionInitializers +- TabPaneInitializers +- RecordBlockInitializers + +## x-designer + +- CalendarV2.Designer + +## x-settings + +- CalendarSettings + +## UI Schema + +### 日历区块 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "CalendarV2.Designer", + "x-component": "CardItem", + "x-decorator": "CalendarBlockProvider", + "x-acl-action": "users:list", + "x-decorator-props": { + "action": "list", + "params": { + "paginate": false + }, + "resource": "users", + "collection": "users", + "fieldNames": { + "id": "id", + "end": ["f_nxqfedh5fd5"], + "start": ["f_nxqfedh5fd5"], + "title": "nickname" + } + }, + "_isJSONSchemaObject": true, + "properties": { + "puytl7p5x8o": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2", + "x-component-props": { + "useProps": "{{ useCalendarBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "toolBar": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2.ActionBar", + "x-initializer": "CalendarActionInitializers", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "_isJSONSchemaObject": true, + "x-uid": "494h1j6bxyg", + "x-async": false, + "x-index": 1 + }, + "event": { + "type": "void", + "version": "2.0", + "x-component": "CalendarV2.Event", + "_isJSONSchemaObject": true, + "properties": { + "drawer": { + "type": "void", + "title": "{{ t(\"View record\") }}", + "version": "2.0", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "_isJSONSchemaObject": true, + "properties": { + "tabs": { + "type": "void", + "version": "2.0", + "x-component": "Tabs", + "x-initializer": "TabPaneInitializers", + "x-component-props": {}, + "_isJSONSchemaObject": true, + "x-initializer-props": { + "gridInitializer": "RecordBlockInitializers" + }, + "properties": { + "tab1": { + "type": "void", + "title": "{{t(\"Details\")}}", + "version": "2.0", + "x-designer": "Tabs.Designer", + "x-component": "Tabs.TabPane", + "x-component-props": {}, + "_isJSONSchemaObject": true, + "properties": { + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "_isJSONSchemaObject": true, + "x-initializer-props": { + "actionInitializers": "CalendarFormActionInitializers" + }, + "x-uid": "yty9wg20qq0", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "hcsgenoc3hb", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "c5cpptwxw7s", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "c5iarcnpefc", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "080x8qlpn6s", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "pcche4s596p", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "gk2jk3gpo9f", + "x-async": false, + "x-index": 1 +} +``` + +### 关系的日历区块 + +```json +{ + "x-uid": "8q0hsbdr9fc", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:list", + "x-decorator": "CalendarBlockProvider", + "x-decorator-props": { + "collection": "b", + "resource": "a.o2m", + "action": "list", + "fieldNames": { + "id": "id", + "start": "createdAt", + "title": "f_ex4581dfc0t" + }, + "params": { + "paginate": false + }, + "association": "a.o2m" + }, + "x-designer": "CalendarV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系的日历区块" + }, + "properties": { + "v3za6qejuz7": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2", + "x-component-props": { + "useProps": "{{ useCalendarBlockProps }}" + }, + "properties": { + "toolBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2.ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-initializer": "CalendarActionInitializers", + "x-uid": "u9ntffsdsrh", + "x-async": false, + "x-index": 1 + }, + "event": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CalendarV2.Event", + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "x-initializer-props": { + "gridInitializer": "RecordBlockInitializers" + }, + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer-props": { + "actionInitializers": "CalendarFormActionInitializers" + }, + "x-initializer": "RecordBlockInitializers", + "x-uid": "2swhiw4lee5", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "z6odjfbwnht", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mxr7ikhfc9y", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mfgucnj7ev8", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "05l5a2h42pn", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "v2jazzvqa85", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/charts.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/charts.md new file mode 100644 index 0000000000..77dbdbdbb9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/charts.md @@ -0,0 +1,143 @@ +# Charts + +## UI Schema + +### 图表区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CardItem", + "x-component-props": { + "name": "charts" + }, + "x-designer": "ChartV2BlockDesigner", + "properties": { + "u8841bjm65y": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-decorator": "ChartV2Block", + "x-initializer": "ChartInitializers", + "x-uid": "j4hzcpzcc3g", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ldc2aokaop2", + "x-async": false, + "x-index": 1 +} +``` + +### 内嵌区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "CardItem", + "x-component-props": { + "name": "charts" + }, + "x-designer": "ChartV2BlockDesigner", + "properties": { + "g3jq3vkx9fv": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-decorator": "ChartV2Block", + "x-initializer": "ChartInitializers", + "properties": { + "ue7lfigjgm1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Row", + "properties": { + "43ztg8gu03v": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Col", + "properties": { + "n47fsb4i2lm": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "ChartRendererProvider", + "x-decorator-props": { + "query": { + "measures": [ + { + "field": ["id"] + } + ], + "dimensions": [], + "filter": { + "$and": [] + }, + "orders": [], + "cache": {} + }, + "config": { + "chartType": "Built-in.line", + "general": { + "xField": "id", + "yField": "id", + "smooth": false, + "isStack": false + } + }, + "collection": "a", + "mode": "builder" + }, + "x-acl-action": "a:list", + "x-designer": "ChartRenderer.Designer", + "x-component": "CardItem", + "x-component-props": { + "size": "small" + }, + "x-initializer": "ChartInitializers", + "properties": { + "y25sgh5pukl": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "ChartRenderer", + "x-component-props": {}, + "x-uid": "ma0s07k753t", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "k6naqcx59ow", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "o8ktlccovml", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mvn9ai9rhx8", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "mcbyby93j6n", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "sn0lczwtkgm", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/details.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/details.md new file mode 100644 index 0000000000..31f765bacb --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/details.md @@ -0,0 +1,169 @@ +# Details + +## UI Schema + +### 详情区块(带分页) + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "DetailsDesigner", + "x-component": "CardItem", + "x-decorator": "DetailsBlockProvider", + "x-acl-action": "users:view", + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 1 + }, + "rowKey": "id", + "resource": "users", + "collection": "users", + "readPretty": true + }, + "_isJSONSchemaObject": true, + "properties": { + "mjad1dcywip": { + "type": "void", + "version": "2.0", + "x-component": "Details", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useDetailsBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "8avswx6qgyp": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "DetailsActionInitializers", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "_isJSONSchemaObject": true, + "x-uid": "01w17yleng8", + "x-async": false, + "x-index": 1 + }, + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "_isJSONSchemaObject": true, + "x-uid": "bgu0ix4lfmc", + "x-async": false, + "x-index": 2 + }, + "pagination": { + "type": "void", + "version": "2.0", + "x-component": "Pagination", + "x-component-props": { + "useProps": "{{ useDetailsPaginationProps }}" + }, + "_isJSONSchemaObject": true, + "x-uid": "6riuxghrz30", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "3apkfu8ngrp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "yeodhismy2w", + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系区块 + +```json +{ + "x-uid": "2gm2iw937q5", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "DetailsBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 1 + }, + "rowKey": "id" + }, + "x-designer": "DetailsDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "关系区块" + }, + "properties": { + "paj8b2noc1g": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Details", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useDetailsBlockProps }}" + }, + "properties": { + "3jisjip7503": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "DetailsActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "cbkfxafywtx", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "d1ls1w6gpzm", + "x-async": false, + "x-index": 2 + }, + "pagination": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Pagination", + "x-component-props": { + "useProps": "{{ useDetailsPaginationProps }}" + }, + "x-uid": "u0rzv8370za", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "1hfu8jbzzqw", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form-read-pretty.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form-read-pretty.md new file mode 100644 index 0000000000..676df30347 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form-read-pretty.md @@ -0,0 +1,286 @@ +# Form (Read pretty) + +## UI Schema + +### 单数据详情 + +```json +{ + "x-uid": "dt4q817fw81", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a", + "collection": "a", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "Form read pretty" + }, + "properties": { + "30qrxij609o": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "fznn1ygt50o", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "t3o4t8fi7l7", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "6dp2qqoc4s0", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系数据详情 + +```json +{ + "x-uid": "jf470wrzdaf", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2oBelongsTo:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2oBelongsTo", + "collection": "b", + "association": "a.o2oBelongsTo", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "对一关系详情" + }, + "properties": { + "uauk2wbj3iq": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "r3pcykksp8l", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "vs000etludp", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "rt228uhudcf", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 表格详情 + +```json +{ + "x-uid": "lxcjjkkor1m", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "表格详情" + }, + "properties": { + "7pe17tcgtye": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "wek8nc795ja", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "5y5u8qm6yvz", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "5bpcm982n51", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 查看关系数据 + +```json +{ + "x-uid": "5ymsrq1fv37", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:get", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "get", + "useParams": "{{ useParamsFromRecord }}", + "useSourceId": "{{ useSourceIdFromParentRecord }}" + }, + "x-designer": "FormV2.ReadPrettyDesigner", + "x-component": "CardItem", + "x-component-props": { + "title": "查看关系数据" + }, + "properties": { + "vehcfr0fsh2": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ReadPrettyFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-uid": "zpuz629u2dg", + "x-async": false, + "x-index": 1 + }, + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-uid": "b8u8e2k5inf", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "2rz0u3j14px", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form.md new file mode 100644 index 0000000000..442457ba05 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/form.md @@ -0,0 +1,418 @@ +# Form + +## UI Schema + +### 添加表单 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a", + "collection": "a" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": {}, + "properties": { + "23pj9m7ot5a": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "0ctuwnatyzd", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "properties": { + "j7ta88et3mv": { + "_isJSONSchemaObject": true, + "version": "2.0", + "title": "{{ t(\"Submit\") }}", + "x-action": "submit", + "x-component": "Action", + "x-designer": "Action.Designer", + "x-component-props": { + "type": "primary", + "htmlType": "submit", + "useProps": "{{ useCreateActionProps }}" + }, + "x-action-settings": { + "triggerWorkflows": [] + }, + "type": "void", + "x-uid": "ua858zswy37", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "o0riryr0ob0", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "1ng4hies4yu", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ebiyvziafda", + "x-async": false, + "x-index": 1 +} +``` + +### 编辑表单 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": false + }, + "x-acl-action": "a:update", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": "get", + "resource": "a", + "collection": "a" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": {}, + "properties": { + "w533dipin7p": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "otncpmalhjr", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "UpdateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "qjjmm1qkapx", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "n12zo6qd72g", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "zafyvmng9m5", + "x-async": false, + "x-index": 1 +} +``` + +### 关系新增表单 + +```json +{ + "x-uid": "il5q341k4vg", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a.o2m:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": null, + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系新增表单" + }, + "properties": { + "3357yvwe4je": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "88xhnoml321", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "dhs8o8vpl1h", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "h6fhh9ox9ay", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系新增(关系表格里的 Add new) + +useSourceIdFromParentRecord 和 useParamsFromRecord 参数缺失 + +```json +{ + "x-uid": "6tknohm4ubr", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": true + }, + "x-acl-action": "a.o2m:create", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系新增表单-Table" + }, + "properties": { + "o96izt6ez7m": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "properties": { + "pk9l1i26yq6": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Row", + "properties": { + "32wz2lr9cry": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid.Col", + "properties": { + "id": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "string", + "x-designer": "FormItem.Designer", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "b.id", + "x-component-props": {}, + "x-read-pretty": true, + "x-uid": "jb09nj4kbfm", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "rtx3b0uom75", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5gvd7ks6m0z", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "fcw1y2xk6km", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "CreateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "msgopb1meoq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "g0mg6fhho4x", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 关系编辑 + +```json +{ + "x-uid": "kzg1tz339ro", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action-props": { + "skipScopeCheck": false + }, + "x-acl-action": "a.o2m:update", + "x-decorator": "FormBlockProvider", + "x-decorator-props": { + "useSourceId": "{{ useSourceIdFromParentRecord }}", + "useParams": "{{ useParamsFromRecord }}", + "action": "get", + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m" + }, + "x-designer": "FormV2.Designer", + "x-component": "CardItem", + "x-component-props": { + "title": "关系编辑表单" + }, + "properties": { + "3jqysycq8m0": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FormItemInitializers", + "x-uid": "sy3sjswogh4", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "UpdateFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "marginTop": 24 + } + }, + "x-uid": "xarcyjpypsy", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "lepez9684rm", + "x-async": false, + "x-index": 1 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/gantt.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/gantt.md new file mode 100644 index 0000000000..4b7ad1269c --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/gantt.md @@ -0,0 +1,185 @@ +# Gantt + +## UI Schema + +### 甘特图区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "users:list", + "x-decorator": "GanttBlockProvider", + "x-decorator-props": { + "collection": "users", + "resource": "users", + "action": "list", + "fieldNames": { + "id": "id", + "start": "createdAt", + "range": "day", + "title": "nickname", + "end": "f_46b1u4dp820" + }, + "params": { + "paginate": false + } + }, + "x-designer": "Gantt.Designer", + "x-component": "CardItem", + "properties": { + "rx21gtpcj3w": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Gantt", + "x-component-props": { + "useProps": "{{ useGanttBlockProps }}" + }, + "properties": { + "toolBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 24 + } + }, + "x-initializer": "GanttActionInitializers", + "x-uid": "m0uru68ugw9", + "x-async": false, + "x-index": 1 + }, + "table": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-decorator": "div", + "x-decorator-props": { + "style": { + "float": "left", + "maxWidth": "35%" + } + }, + "x-initializer": "TableColumnInitializers", + "x-component": "TableV2", + "x-component-props": { + "rowKey": "id", + "rowSelection": { + "type": "checkbox" + }, + "useProps": "{{ useTableBlockProps }}", + "pagination": false + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Actions\") }}", + "x-action-column": "actions", + "x-decorator": "TableV2.Column.ActionBar", + "x-component": "TableV2.Column", + "x-designer": "TableV2.ActionColumnDesigner", + "x-initializer": "TableActionColumnInitializers", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "DndContext", + "x-component": "Space", + "x-component-props": { + "split": "|" + }, + "x-uid": "z2vnizu7bds", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "8gbabo6kdr6", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5c8g2x6rvro", + "x-async": false, + "x-index": 2 + }, + "detail": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Gantt.Event", + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "jr5h3asz6gp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "8uwfpgyok9x", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "bjm4sfwxmrx", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "7cmwpc0rrwj", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nffb7qbs96o", + "x-async": false, + "x-index": 3 + } + }, + "x-uid": "hoi2j5lhlho", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "e0rdive4xpa", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/grid-card.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/grid-card.md new file mode 100644 index 0000000000..6620d607bc --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/grid-card.md @@ -0,0 +1,219 @@ +# GridCard + +## UI Schema + +### 栅格卡片 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "GridCard.Designer", + "x-component": "BlockItem", + "x-decorator": "GridCard.Decorator", + "x-acl-action": "users:view", + "x-component-props": { + "useProps": "{{ useGridCardBlockItemProps }}" + }, + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 12 + }, + "rowKey": "id", + "resource": "users", + "collection": "users", + "readPretty": true, + "runWhenParamsChanged": true + }, + "_isJSONSchemaObject": true, + "properties": { + "actionBar": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "GridCardActionInitializers", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "_isJSONSchemaObject": true, + "x-uid": "prx9wnlzqb1", + "x-async": false, + "x-index": 1 + }, + "list": { + "type": "array", + "version": "2.0", + "x-component": "GridCard", + "x-component-props": { + "useProps": "{{ useGridCardBlockProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "item": { + "type": "object", + "version": "2.0", + "x-component": "GridCard.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useGridCardItemProps }}" + }, + "_isJSONSchemaObject": true, + "properties": { + "grid": { + "type": "void", + "version": "2.0", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "_isJSONSchemaObject": true, + "x-initializer-props": { + "useProps": "{{ useGridCardItemInitializerProps }}" + }, + "x-uid": "pq895sv6136", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "type": "void", + "version": "2.0", + "x-align": "left", + "x-component": "ActionBar", + "x-initializer": "GridCardItemActionInitializers", + "x-component-props": { + "layout": "one-column", + "useProps": "{{ useGridCardActionBarProps }}" + }, + "_isJSONSchemaObject": true, + "x-uid": "w2c3qcp4nme", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "1ct8yn1jnzn", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "pvnkvtc6tte", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "phodsqwsifq", + "x-async": false, + "x-index": 1 +} +``` + +### 关系数据的栅格卡片 + +```json +{ + "x-uid": "yr79zvl98lc", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "GridCard.Decorator", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 12 + }, + "runWhenParamsChanged": true, + "columnCount": { + "xs": 1, + "md": 12, + "lg": 12, + "xxl": 12 + } + }, + "x-component": "BlockItem", + "x-component-props": { + "useProps": "{{ useGridCardBlockItemProps }}" + }, + "x-designer": "GridCard.Designer", + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "GridCardActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "u36l44beq6a", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "GridCard", + "x-component-props": { + "useProps": "{{ useGridCardBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "GridCard.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useGridCardItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useGridCardItemInitializerProps }}" + }, + "x-uid": "aqxlonv3bpk", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "GridCardItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useGridCardActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "zxhqo81kdf5", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "ed56owlyz1p", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nfpuvrkwkk7", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md new file mode 100644 index 0000000000..a88afb3b92 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/index.md @@ -0,0 +1,255 @@ +# 数据区块概述 + +数据可能是某个表(collection)的数据,也可能是某个关系(association)的数据 + +- collection,如 a 表 +- association,如 a.b 关系,关系表为 b + +collection 示例 + +```js +{ + "collection": "a" +} +``` + +association 示例 + +```js +{ + "collection": "b", + "association": "a.b" +} +``` + +数据的操作 + +```bash +: +.: +``` + +实际的请求参数 + +``` + /api/: + /api/:/ + /api///: + /api///:/ +``` + +- collection、association、action 是配置里存好的 +- sourceId、filterByTk 由上下文提供 + +## RecordProvider + +Record + +```ts +class Record { + protected current = {}; + protected parent?: Record; + public isNew = false; + constructor(options = {}) { + const { current, parent, isNew } = options; + this.current = current || {}; + this.parent = parent; + this.isNew = isNew; + } +} +``` + +a:list(**list 外层不套空 RecordPicker**) + +```jsx | pure +{list.map((item) => { + const record = new Record({ current: item }); + return +})} +``` + +a:get(view、edit) + +```jsx | pure +const record = new Record({ current: item }); + +``` + +a:create + +```jsx | pure +const record = new Record({ current, isNew: true }); + +``` + +a.b:list + +```jsx | pure + + {list.map((item) => { + const recordB = new Record({ + current: item, + }); + return + })} + +``` + +a.b:get + +```jsx | pure +const recordA = new Record({ + current: itemA, +}); +const recordB = new Record({ + current: itemB, + parent: recordA, +}); +// 或者 +recordB.setParent(recordA); + + + + +``` + +a.b:create + +```jsx | pure +const recordA = new Record({ + current: itemA, +}); +const recordB = new Record({ + isNew: true, + parent: recordA, +}); + + + +``` + +## 区块 + +以下重要组件说明 + +- `DataBlockProvider` 数据区块的总称 +- `BlockProvider` 区块的各种参数设置 +- `CollectionProvider` collection 信息 +- `AssociationProvider` association 信息 +- `UseRequestProvider` useRequest 的 result + +没有当前记录的区块,如表格 + +```tsx | pure + + + + + + +
+ + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + {props.children} + + + +``` + +有当前记录的区块,如表单 + +```tsx | pure + +
+ +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + {props.children} + + + + +``` + +有当前父级记录的区块,如对多关系的表格区块,区块本身不套 RecordPicker + +```tsx | pure + + + + + + + +
+ + + + + + + + +
+ + + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + {props.children} + + + + +``` + +有父级记录也有当前记录的区块,如关系的表单 + +```tsx | pure + + + + + + + + +``` + +DataBlockProvider 内容 + +```tsx | pure + + + + + + {props.children} + + + + + +``` diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/kanban.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/kanban.md new file mode 100644 index 0000000000..b84b1b62a4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/kanban.md @@ -0,0 +1,160 @@ +# Kanban + +## UI Schema + +### 看板区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "t_ehwqkw0ljw2:list", + "x-decorator": "KanbanBlockProvider", + "x-decorator-props": { + "collection": "t_ehwqkw0ljw2", + "resource": "t_ehwqkw0ljw2", + "action": "list", + "groupField": "f_5h4umwf59lc", + "params": { + "sort": ["f_5h4umwf59lc_sort"], + "paginate": false + } + }, + "x-designer": "Kanban.Designer", + "x-component": "CardItem", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "KanbanActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "4vh5t2iohwp", + "x-async": false, + "x-index": 1 + }, + "iwnc45ou960": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "Kanban", + "x-component-props": { + "useProps": "{{ useKanbanBlockProps }}" + }, + "properties": { + "card": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-read-pretty": true, + "x-label-disabled": true, + "x-decorator": "BlockItem", + "x-component": "Kanban.Card", + "x-component-props": { + "openMode": "drawer" + }, + "x-designer": "Kanban.Card.Designer", + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-component-props": { + "dndContext": false + }, + "x-uid": "3v010y09684", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "dk9a5cb9d95", + "x-async": false, + "x-index": 1 + }, + "cardViewer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"View\") }}", + "x-designer": "Action.Designer", + "x-component": "Kanban.CardViewer", + "x-action": "view", + "x-component-props": { + "openMode": "drawer" + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"View record\") }}", + "x-component": "Action.Container", + "x-component-props": { + "className": "nb-action-popup" + }, + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "1lhiyyydpye", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "gisuj0zldz1", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "ohly2w4gnwp", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "29hjrxcfczh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "0k56h1q80xo", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "uny2d4n5ry4", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "3gga5lop4t6", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/list.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/list.md new file mode 100644 index 0000000000..4e690cb7d9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/list.md @@ -0,0 +1,213 @@ +# List + +## UI Schema + +### 列表区块 + +```json +{ + "x-uid": "h9mzrnxokk3", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a:view", + "x-decorator": "List.Decorator", + "x-decorator-props": { + "resource": "a", + "collection": "a", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 10 + }, + "runWhenParamsChanged": true, + "rowKey": "id" + }, + "x-component": "CardItem", + "x-designer": "List.Designer", + "x-component-props": { + "title": "List 区块" + }, + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ListActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "chjgpgd45qz", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "List", + "x-component-props": { + "props": "{{ useListBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "List.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useListItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useListItemInitializerProps }}" + }, + "x-uid": "u45nwmwlone", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "ListItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useListActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "9erso7nhotq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "qkuzlun8frh", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "1mfaa48qy5j", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系的列表区块 + +```json +{ + "x-uid": "6tzpnkz60rx", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "a.o2m:view", + "x-decorator": "List.Decorator", + "x-decorator-props": { + "resource": "a.o2m", + "collection": "b", + "association": "a.o2m", + "readPretty": true, + "action": "list", + "params": { + "pageSize": 10 + }, + "runWhenParamsChanged": true + }, + "x-component": "CardItem", + "x-designer": "List.Designer", + "x-component-props": { + "title": "对多关系的列表区块" + }, + "properties": { + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "ListActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "i05xr7nfs26", + "x-async": false, + "x-index": 1 + }, + "list": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-component": "List", + "x-component-props": { + "props": "{{ useListBlockProps }}" + }, + "properties": { + "item": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "object", + "x-component": "List.Item", + "x-read-pretty": true, + "x-component-props": { + "useProps": "{{ useListItemProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "ReadPrettyFormItemInitializers", + "x-initializer-props": { + "useProps": "{{ useListItemInitializerProps }}" + }, + "x-uid": "krerg70a28e", + "x-async": false, + "x-index": 1 + }, + "actionBar": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-align": "left", + "x-initializer": "ListItemActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "useProps": "{{ useListActionBarProps }}", + "layout": "one-column" + }, + "x-uid": "9745q36ltfv", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "yxc0lnax9gk", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "6mlzb7p6dvn", + "x-async": false, + "x-index": 2 + } + }, + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/map.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/map.md new file mode 100644 index 0000000000..0413da64be --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/map.md @@ -0,0 +1,115 @@ +# Map + +## UI Schema + +### 地图区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-acl-action": "t_ddo5bd0swdg:list", + "x-decorator": "MapBlockProvider", + "x-decorator-props": { + "collection": "t_ddo5bd0swdg", + "resource": "t_ddo5bd0swdg", + "action": "list", + "fieldNames": { + "field": ["f_tmhy5bbusr8"] + }, + "params": { + "paginate": false + } + }, + "x-designer": "MapBlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "MapActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": 16 + } + }, + "x-uid": "ap0xzy32bms", + "x-async": false, + "x-index": 1 + }, + "1672zefmplu": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "MapBlock", + "x-component-props": { + "useProps": "{{ useMapBlockProps }}" + }, + "properties": { + "drawer": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Action.Drawer", + "x-component-props": { + "className": "nb-action-popup" + }, + "title": "{{ t(\"View record\") }}", + "properties": { + "tabs": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Tabs", + "x-component-props": {}, + "x-initializer": "TabPaneInitializers", + "properties": { + "tab1": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{t(\"Details\")}}", + "x-component": "Tabs.TabPane", + "x-designer": "Tabs.Designer", + "x-component-props": {}, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "RecordBlockInitializers", + "x-uid": "vce3u8hfdrf", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "waw7z3qw7nn", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "szyybarlapl", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "83m07ksqbal", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "5cquhqq2161", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "p42awfpfnbf", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/data/table.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/table.md new file mode 100644 index 0000000000..4c99e25545 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/data/table.md @@ -0,0 +1,193 @@ +# Table + +## UI Schema + +### 表格区块 + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-decorator": "TableBlockProvider", + "x-acl-action": "users:list", + "x-filter-targets": [], + "x-decorator-props": { + "action": "list", + "params": { + "pageSize": 20 + }, + "rowKey": "id", + "dragSort": false, + "resource": "users", + "showIndex": true, + "collection": "users", + "disableTemplate": false + }, + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "version": "2.0", + "x-component": "ActionBar", + "x-initializer": "TableActionInitializers", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "_isJSONSchemaObject": true, + "x-uid": "55tgsn1puqk", + "x-async": false, + "x-index": 1 + }, + "g22il5nkv67": { + "type": "array", + "version": "2.0", + "x-component": "TableV2", + "x-initializer": "TableColumnInitializers", + "x-component-props": { + "rowKey": "id", + "useProps": "{{ useTableBlockProps }}", + "rowSelection": { + "type": "checkbox" + } + }, + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "title": "{{ t(\"Actions\") }}", + "version": "2.0", + "x-designer": "TableV2.ActionColumnDesigner", + "x-component": "TableV2.Column", + "x-decorator": "TableV2.Column.ActionBar", + "x-initializer": "TableActionColumnInitializers", + "x-action-column": "actions", + "_isJSONSchemaObject": true, + "properties": { + "actions": { + "type": "void", + "version": "2.0", + "x-component": "Space", + "x-decorator": "DndContext", + "x-component-props": { + "split": "|" + }, + "_isJSONSchemaObject": true, + "x-uid": "qkcugf5l0h6", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "kbgbskxsj3j", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "6r8e3mhnuxg", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "b87bbc5x0cp", + "x-async": false, + "x-index": 1 +} +``` + +### 对多关系区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "TableBlockProvider", + "x-acl-action": "a.o2m:list", + "x-decorator-props": { + "collection": "b", + "resource": "a.o2m", + "action": "list", + "params": { + "pageSize": 20 + }, + "showIndex": true, + "dragSort": false, + "disableTemplate": false, + "association": "a.o2m" + }, + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "TableActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "style": { + "marginBottom": "var(--nb-spacing)" + } + }, + "x-uid": "0gwze38mzps", + "x-async": false, + "x-index": 1 + }, + "0wuk1cpy5zp": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "array", + "x-initializer": "TableColumnInitializers", + "x-component": "TableV2", + "x-component-props": { + "rowKey": "id", + "rowSelection": { + "type": "checkbox" + }, + "useProps": "{{ useTableBlockProps }}" + }, + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "{{ t(\"Actions\") }}", + "x-action-column": "actions", + "x-decorator": "TableV2.Column.ActionBar", + "x-component": "TableV2.Column", + "x-designer": "TableV2.ActionColumnDesigner", + "x-initializer": "TableActionColumnInitializers", + "properties": { + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "DndContext", + "x-component": "Space", + "x-component-props": { + "split": "|" + }, + "x-uid": "gq0nkc7rf0q", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "zx97354nzfq", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "nss8uocacdq", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "0j5kcrius5z", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/collapse.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/collapse.md new file mode 100644 index 0000000000..0bb30600a9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/collapse.md @@ -0,0 +1,41 @@ +# Collapse + +## UI Schema + +### 折叠面板 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "AssociationFilter.Provider", + "x-decorator-props": { + "collection": "users", + "blockType": "filter", + "associationFilterStyle": { + "width": "100%" + }, + "name": "filter-collapse" + }, + "x-designer": "AssociationFilter.BlockDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "properties": { + "x8q0r7wp73e": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-action": "associateFilter", + "x-initializer": "AssociationFilter.FilterBlockInitializer", + "x-component": "AssociationFilter", + "x-uid": "j11suxuizte", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "0g6pyonrj83", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/form.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/form.md new file mode 100644 index 0000000000..f0145c51b7 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/filter/form.md @@ -0,0 +1,63 @@ +# Form + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "FilterFormBlockProvider", + "x-decorator-props": { + "resource": "users", + "collection": "users" + }, + "x-designer": "FormV2.FilterDesigner", + "x-component": "CardItem", + "x-filter-targets": [], + "x-filter-operators": {}, + "properties": { + "dwltvxybiir": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "FormV2", + "x-component-props": { + "useProps": "{{ useFormBlockProps }}" + }, + "properties": { + "grid": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "FilterFormItemInitializers", + "x-uid": "aggw48stzwc", + "x-async": false, + "x-index": 1 + }, + "actions": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-initializer": "FilterFormActionInitializers", + "x-component": "ActionBar", + "x-component-props": { + "layout": "one-column", + "style": { + "float": "right" + } + }, + "x-uid": "7aogdo89a7s", + "x-async": false, + "x-index": 2 + } + }, + "x-uid": "wreuz6dr1pc", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "7d1o5bruekl", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/index.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/index.md new file mode 100644 index 0000000000..07dd0c5c77 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/index.md @@ -0,0 +1 @@ +# Overview diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/others/iframe.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/iframe.md new file mode 100644 index 0000000000..58a2bf0380 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/iframe.md @@ -0,0 +1,22 @@ + +# iframe + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "Iframe.Designer", + "x-component": "Iframe", + "x-decorator": "BlockItem", + "x-component-props": {}, + "x-decorator-props": { + "name": "iframe" + }, + "_isJSONSchemaObject": true, + "x-uid": "fmmlqe8rrei", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/others/markdown.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/markdown.md new file mode 100644 index 0000000000..478fafd2ce --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/markdown.md @@ -0,0 +1,24 @@ +# Markdown + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "Markdown.Void.Designer", + "x-editable": false, + "x-component": "Markdown.Void", + "x-decorator": "CardItem", + "x-component-props": { + "content": "This is a demo text, **supports Markdown syntax**." + }, + "x-decorator-props": { + "name": "markdown" + }, + "_isJSONSchemaObject": true, + "x-uid": "w75e9d5vs1e", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/blocks/others/workflow-todo.md b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/workflow-todo.md new file mode 100644 index 0000000000..9f477e5d78 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/blocks/others/workflow-todo.md @@ -0,0 +1,33 @@ +# Workflow todos + +## x-designer + +- TableBlockDesigner(不适合直接用表格区块的) + +## UI Schema + +```json +{ + "type": "void", + "version": "2.0", + "x-designer": "TableBlockDesigner", + "x-component": "CardItem", + "x-decorator": "WorkflowTodo.Decorator", + "x-decorator-props": {}, + "_isJSONSchemaObject": true, + "properties": { + "todos": { + "type": "void", + "version": "2.0", + "x-component": "WorkflowTodo", + "_isJSONSchemaObject": true, + "x-uid": "xq1qcdpq7rq", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "3z3ey8r6ik9", + "x-async": false, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/cascader-select.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/cascader-select.md new file mode 100644 index 0000000000..94547e50cb --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/cascader-select.md @@ -0,0 +1 @@ +# 级联选择器 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/file-manager.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/file-manager.md new file mode 100644 index 0000000000..5b02970f76 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/file-manager.md @@ -0,0 +1 @@ +# 文件管理器 diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/index.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/index.md new file mode 100644 index 0000000000..e9f57784b4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/index.md @@ -0,0 +1,21 @@ +# 关系字段组件 + +### 标题 Read pretty + +### 标签 Read pretty + +### 子详情 Read pretty + +### 子表单 Editable + +### 子表单(弹窗) Editable + +### 下拉选择器 Editable + +### 数据选择器 Editable + +### 级联选择器 Editable + +### 文件管理器 Editable + +### 子表格 Editable Read pretty diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/record-picker.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/record-picker.md new file mode 100644 index 0000000000..ae439f012a --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/record-picker.md @@ -0,0 +1 @@ +# 数据选择器 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/select.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/select.md new file mode 100644 index 0000000000..9df4627f7c --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/select.md @@ -0,0 +1 @@ +# 下拉选择器 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-details.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-details.md new file mode 100644 index 0000000000..a509906e4f --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-details.md @@ -0,0 +1 @@ +# 子详情 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form-popover.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form-popover.md new file mode 100644 index 0000000000..acc10f4a72 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form-popover.md @@ -0,0 +1 @@ +# 子表单 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form.md new file mode 100644 index 0000000000..acc10f4a72 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-form.md @@ -0,0 +1 @@ +# 子表单 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-table.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-table.md new file mode 100644 index 0000000000..9556a6c5ce --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/sub-table.md @@ -0,0 +1 @@ +# 子表格 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/tag.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/tag.md new file mode 100644 index 0000000000..f927916b34 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/tag.md @@ -0,0 +1 @@ +# 标签 diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/title.md b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/title.md new file mode 100644 index 0000000000..e8a280714e --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/association-components/title.md @@ -0,0 +1 @@ +# 标题 \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/form-item.md b/packages/core/client/docs/zh-CN/ui-schema/fields/form-item.md new file mode 100644 index 0000000000..74112ffb22 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/form-item.md @@ -0,0 +1,22 @@ +# FormItem - 表单项 + +## UI Schema + +### 表单字段 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "string", + "x-designer": "FormItem.Designer", + "x-component": "CollectionField", + "x-decorator": "FormItem", + "x-collection-field": "a.id", + "x-component-props": {}, + "x-read-pretty": true, + "x-uid": "u2nk80cguoa", + "x-async": false, + "x-index": 1 +} +``` diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/index.md b/packages/core/client/docs/zh-CN/ui-schema/fields/index.md new file mode 100644 index 0000000000..e734184dbe --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/index.md @@ -0,0 +1,5 @@ +# Overview + +## UI Schema 协议 + +- `x-collection-field` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/fields/table-column.md b/packages/core/client/docs/zh-CN/ui-schema/fields/table-column.md new file mode 100644 index 0000000000..61598b8f6a --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/fields/table-column.md @@ -0,0 +1,40 @@ +# 表格列 + +## UI Schema + +### 表格列 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-decorator": "TableV2.Column.Decorator", + "x-designer": "TableV2.Column.Designer", + "x-component": "TableV2.Column", + "properties": { + "f_beilggsbj0r": { + "_isJSONSchemaObject": true, + "version": "2.0", + "x-collection-field": "a.f_beilggsbj0r", + "x-component": "CollectionField", + "x-component-props": { + "ellipsis": true + }, + "x-read-pretty": true, + "x-decorator": null, + "x-decorator-props": { + "labelStyle": { + "display": "none" + } + }, + "x-uid": "h5u4xi7uvji", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "z8p8ezctwsh", + "x-async": false, + "x-index": 2 +} +``` diff --git a/packages/core/client/docs/zh-CN/ui-schema/globals/menu.md b/packages/core/client/docs/zh-CN/ui-schema/globals/menu.md new file mode 100644 index 0000000000..69a4e6a800 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/globals/menu.md @@ -0,0 +1,127 @@ +# Menu + + +菜单模块以后会重构,Item 直接用 items 对接,不用 schema 嵌套,Menu 也会开放到更多场景里使用。 + + +## `x-initializer` + +- MenuItemInitializers + +## `x-designer` + +- Menu.Designer + +## `x-settings` + +- MenuSettings + +## UI Schema + +### 菜单 + +```json +{ + "type": "void", + "x-component": "Menu", + "x-designer": "Menu.Designer", + "x-initializer": "MenuItemInitializers", + "x-component-props": { + "mode": "mix", + "theme": "dark", + "onSelect": "{{ onSelect }}", + "sideMenuRefScopeKey": "sideMenuRef" + }, + "name": "lap9zil5z4u", + "x-uid": "x77wpukk3qz", + "x-async": false +} +``` + +### 菜单分组 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Group", + "x-component": "Menu.SubMenu", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "icon": "alipaycircleoutlined", + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "zjxw5sph3do", + "x-async": false, + "x-index": 1 +} +``` + +### 菜单页面 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Page1", + "x-component": "Menu.Item", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "icon": "alipaycircleoutlined", + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "y0jp661nb8i", + "x-async": false, + "x-index": 2 +} +``` + +### 链接 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Link", + "x-component": "Menu.URL", + "x-decorator": "ACLMenuItemProvider", + "x-component-props": { + "href": "#", + "icon": "alipaycircleoutlined" + }, + "x-server-hooks": [ + { + "type": "onSelfCreate", + "method": "bindMenuToRole" + }, + { + "type": "onSelfSave", + "method": "extractTextToLocale" + } + ], + "x-uid": "ynembhzwcj1", + "x-async": false, + "x-index": 3 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/globals/page.md b/packages/core/client/docs/zh-CN/ui-schema/globals/page.md new file mode 100644 index 0000000000..2f93084f78 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/globals/page.md @@ -0,0 +1,94 @@ +# Page + + +现在页面依赖菜单,以后会独立出来,也可以创建无菜单的页面。另外,页面的标签页的设计还有问题。 + + +## `x-initializer` + +- PageTabsInitializers(暂无) +- BlockInitializers + +## `x-designer` + +- PageDesigner(暂无) +- PageTabsDesigner(暂无) + +## `x-settings` + +- PageSettings + +## UI Schema + +### 页面区块 + +```json +{ + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Page", + "title": "AA333", + "x-component-props": { + "enablePageTabs": true, + "hidePageTitle": true, + "disablePageHeader": true + }, + "properties": { + "nzxygx0iady": { + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "x-uid": "z8hxh8fxksb", + "x-async": false, + "x-index": 1 + } + }, + "x-uid": "9stdz1ld2p5", + "x-async": true, + "x-index": 1 +} +``` + +### 页面标签页 + +```json +{ + "x-uid": "cqq0ythy0yj", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Page", + "x-component-props": { + "enablePageTabs": true + }, + "properties": { + "o8gqaximg8c": { + "x-uid": "lzz2tqi8sxp", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "title": "Tab1", + "x-async": false, + "x-index": 1 + }, + "zjl1itzi6t8": { + "x-uid": "x10ru2grypq", + "_isJSONSchemaObject": true, + "version": "2.0", + "type": "void", + "title": "Tab2", + "x-component": "Grid", + "x-initializer": "BlockInitializers", + "x-async": false, + "x-index": 2 + } + }, + "x-async": true, + "x-index": 1 +} +``` \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/ui-schema/globals/tabs.md b/packages/core/client/docs/zh-CN/ui-schema/globals/tabs.md new file mode 100644 index 0000000000..c3ccea4686 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/globals/tabs.md @@ -0,0 +1,5 @@ +# Tabs + + +标签页以后会重构,相关场景有页面的标签页和弹窗里的标签页 + diff --git a/packages/core/client/docs/zh-CN/ui-schema/index.md b/packages/core/client/docs/zh-CN/ui-schema/index.md new file mode 100644 index 0000000000..d908d9ed22 --- /dev/null +++ b/packages/core/client/docs/zh-CN/ui-schema/index.md @@ -0,0 +1,25 @@ +# 概述 + +## `x-initializer` + +不是所有节点都能使用 `x-initializer`,内置的 Schema 组件里,只有 Grid、ActionBar、Tabs、Table 组件有 `x-initializer` 参数 + +- Grid(通用) +- ActionBar(通用) +- Tabs(通用) +- Table +- Menu + +## `x-designer` + +`x-designer` 通常需要和 `BlockItem`、`FormItem`、`CardItem` 这类组件结合使用 + +完整的 Designer 组件有 + +- DragHandler +- Initializer +- Settings + +```tsx | pure + +``` diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 5cb3856ed7..0d7ddb06c4 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -74,7 +74,7 @@ "@types/react-big-calendar": "^1.6.4", "axios-mock-adapter": "^1.20.0", "dumi": "^2.2.0", - "dumi-theme-nocobase": "^0.2.14" + "dumi-theme-nocobase": "^0.2.18" }, "gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644" } diff --git a/packages/core/client/src/__tests__/e2e/action.test.ts b/packages/core/client/src/__tests__/e2e/action.test.ts index ee5617216a..30eb0caf9c 100644 --- a/packages/core/client/src/__tests__/e2e/action.test.ts +++ b/packages/core/client/src/__tests__/e2e/action.test.ts @@ -141,21 +141,21 @@ test.describe('add action & remove action', () => { await page.getByLabel('block-item-CardItem-users-table').click(); await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').click(); //添加按钮 - await page.getByLabel('Enable actions-Filter').click(); - await page.getByLabel('Enable actions-Add new').click(); - await page.getByLabel('Enable actions-Delete').click(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').hover(); await page.getByText('Enable actions').hover(); await expect(page.getByLabel('action-Filter.Action-Filter-filter-users-table')).toBeVisible(); await expect(page.getByLabel('action-Action-Add new-create-users-table')).toBeVisible(); await expect(page.getByLabel('action-Action-Delete-destroy-users-table')).toBeVisible(); - await expect(page.getByLabel('Enable actions-Filter').getByRole('switch')).toBeChecked(); - await expect(page.getByLabel('Enable actions-Add new').getByRole('switch')).toBeChecked(); - await expect(page.getByLabel('Enable actions-Delete').getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); //移除按钮 - await page.getByLabel('Enable actions-Filter').click(); - await page.getByLabel('Enable actions-Add new').click(); - await page.getByLabel('Enable actions-Delete').click(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); await expect(page.getByLabel('action-Action-Add new-create-users-table')).not.toBeVisible(); await expect( page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Delete' }), @@ -163,24 +163,22 @@ test.describe('add action & remove action', () => { await expect( page.getByLabel('block-item-CardItem-users-table').getByLabel('Filter', { exact: true }), ).not.toBeVisible(); - await expect(page.getByLabel('Enable actions-Filter').getByRole('switch')).not.toBeChecked(); - await expect(page.getByLabel('Enable actions-Add new').getByRole('switch')).not.toBeChecked(); - await expect(page.getByLabel('Enable actions-Delete').getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); }); }); test.describe('action drag in block', () => { test('drag th action orders', async ({ page, mockPage }) => { await mockPage({ pageSchema: tablePageSchema }).goto(); - await page.getByLabel('block-item-CardItem-users-table').click(); - await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').click(); - //添加按钮 - await page.getByLabel('Enable actions-Add new').click(); - await page.getByLabel('Enable actions-Delete').click(); - await page.getByLabel('Enable actions-Refresh').click(); await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').hover(); - await page.getByText('Enable actions').hover(); - await page.waitForTimeout(1000); // 等待1秒钟 + //添加按钮 + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + // 向右挪动鼠标指针,用以关闭下拉列表 + await page.mouse.move(300, 0); const addNewBtn = await page.getByLabel('action-Action-Add new-create-users-table'); await addNewBtn.hover(); @@ -205,14 +203,13 @@ test.describe('action drag in block', () => { test.describe('action display config', () => { test('editing action name,icon and color', async ({ page, mockPage }) => { await mockPage({ pageSchema: tablePageSchema }).goto(); - await page.getByLabel('block-item-CardItem-users-table').click(); - await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').click(); + await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').hover(); //添加按钮 - await page.getByLabel('Enable actions-Add new').click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); await page.getByLabel('action-Action-Add new-create-users-table').hover(); await page.getByLabel('action-Action-Add new-create-users-table').getByLabel('designer-schema-settings').hover(); //更新按钮图标、名称、样式 - await page.getByLabel('Edit button').click(); + await page.getByRole('menuitem', { name: 'Edit button' }).click(); await page.getByRole('textbox').fill('Add new1'); await page.getByRole('button', { name: 'close', exact: true }).click(); await page.getByRole('button', { name: 'Select icon', exact: true }).click(); @@ -220,7 +217,7 @@ test.describe('action display config', () => { await page.getByLabel('Danger red').check(); await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect( - await page.getByLabel('block-item-CardItem-users-table').locator('.nb-action-bar').getByLabel('user-add'), + page.getByLabel('block-item-CardItem-users-table').locator('.nb-action-bar').getByLabel('user-add'), ).toBeVisible(); await expect( page.getByLabel('block-item-CardItem-users-table').locator('.nb-action-bar').locator('.ant-btn-dangerous'), @@ -228,18 +225,17 @@ test.describe('action display config', () => { }); test('action open mode ', async ({ page, mockPage }) => { await mockPage({ pageSchema: tablePageSchema }).goto(); - await page.getByLabel('block-item-CardItem-users-table').click(); await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').hover(); //添加按钮 - await page.getByLabel('Enable actions-Add new').click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); await page.getByLabel('action-Action-Add new-create-users-table').click(); - await expect(await page.locator('.ant-drawer')).toBeVisible(); + await expect(page.locator('.ant-drawer')).toBeVisible(); //更新按钮打开方式 await page.locator('.ant-drawer-mask').click(); await page.getByLabel('action-Action-Add new-create-users-table').hover(); await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-users' }).hover(); - await page.getByTitle('Open mode').click(); + await page.getByRole('menuitem', { name: 'Open mode' }).click(); await page.getByRole('option', { name: 'Dialog' }).click(); await page.getByLabel('action-Action-Add new-create-users-table').click(); const drawerComponent = page.getByTestId('modal-Action.Container-users-Add record'); @@ -247,25 +243,24 @@ test.describe('action display config', () => { }); test('setting action model size', async ({ page, mockPage }) => { await mockPage({ pageSchema: tablePageSchema }).goto(); - await page.getByLabel('block-item-CardItem-users-table').click(); await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users').hover(); //添加按钮 - await page.getByLabel('Enable actions-Add new').click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); await page.getByLabel('action-Action-Add new-create-users-table').click(); - await expect(await page.locator('.ant-drawer')).toBeVisible(); + await expect(page.locator('.ant-drawer')).toBeVisible(); await page.locator('.ant-drawer-mask').click(); //修改尺寸 await page.getByLabel('action-Action-Add new-create-users-table').hover(); await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-users' }).hover(); - await page.getByTitle('Popup size').click(); + await page.getByRole('menuitem', { name: 'Popup size' }).click(); //默认尺寸为Middle - await expect(await page.getByTitle('Popup size').locator('.ant-select-selection-item').innerText()).toBe('Middle'); + await expect(page.getByTitle('Popup size').locator('.ant-select-selection-item')).toHaveText('Middle'); //设置为large await page.getByRole('option', { name: 'Large' }).click(); await page.getByLabel('action-Action-Add new-create-users-table').click(); - const drawerItem = await page.locator('.ant-drawer > .ant-drawer-content-wrapper'); + const drawerItem = page.locator('.ant-drawer > .ant-drawer-content-wrapper'); const drawerWidth = await drawerItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); const parent = element.parentElement; @@ -274,7 +269,7 @@ test.describe('action display config', () => { return percentageWidth; }); //宽度为70% - await expect(drawerWidth).toBe(70); + expect(drawerWidth).toBe(70); }); }); @@ -286,26 +281,26 @@ test.describe('action linkage rule', () => { .getByRole('button', { name: 'Actions', exact: true }) .hover(); await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-users').hover(); - await page.getByLabel('Enable actions-View').click(); + await page.getByRole('menuitem', { name: 'View' }).click(); await page.getByLabel('block-item-CardItem-users-table').getByLabel('View').hover(); await page.getByLabel('View').getByLabel('designer-schema-settings').hover(); - await page.getByRole('button', { name: 'Linkage rules' }).click(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule', exact: true }).click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByTestId('filter-select-field').click(); + await page.getByTestId('select-filter-field').click(); await page.getByTitle('ID', { exact: true }).getByText('ID').click(); await page.getByRole('spinbutton').fill('1'); await page.getByText('Add property').click(); await page.getByTestId('select-linkage-properties').click(); await page.getByText('Hidden').click(); - await page.locator('.ant-modal').getByRole('button', { name: 'OK' }).click(); + await page.locator('.ant-modal').getByRole('button', { name: 'OK', exact: true }).click(); //配置中,按钮显示半透明 const actionItem = page.getByLabel('block-item-CardItem-users-table').getByLabel('View'); const inputErrorBorderColor = await actionItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return computedStyle.opacity; }); - await expect(inputErrorBorderColor).toBe('0.1'); + expect(inputErrorBorderColor).toBe('0.1'); //使用中,按钮隐藏 await page.getByRole('button', { name: 'highlight' }).click(); await expect(page.getByLabel('block-item-CardItem-users-table').getByLabel('View')).not.toBeVisible(); @@ -317,26 +312,27 @@ test.describe('action linkage rule', () => { .getByRole('button', { name: 'Actions', exact: true }) .hover(); await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-users').hover(); - await page.getByLabel('Enable actions-View').click(); + await page.getByRole('menuitem', { name: 'View' }).click(); await page.getByLabel('block-item-CardItem-users-table').getByLabel('View').hover(); await page.getByLabel('View').getByLabel('designer-schema-settings').hover(); - await page.getByRole('button', { name: 'Linkage rules' }).click(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule', exact: true }).click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByTestId('filter-select-field').click(); + await page.getByTestId('select-filter-field').click(); await page.getByTitle('ID', { exact: true }).getByText('ID').click(); await page.getByRole('spinbutton').fill('1'); await page.getByText('Add property').click(); await page.getByTestId('select-linkage-properties').click(); await page.getByText('Disabled').click(); - await page.locator('.ant-modal').getByRole('button', { name: 'OK' }).click(); - await page.waitForTimeout(1000); // 等待1秒钟 + await page.locator('.ant-modal').getByRole('button', { name: 'OK', exact: true }).click(); + const linkBtn = page.getByLabel('block-item-CardItem-users-table').getByLabel('View'); const linkBtnCursor = await linkBtn.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return computedStyle.cursor; }); + //按钮禁用 - await expect(linkBtnCursor).toBe('not-allowed'); + expect(linkBtnCursor).toBe('not-allowed'); }); }); diff --git a/packages/core/client/src/__tests__/e2e/block.test.ts b/packages/core/client/src/__tests__/e2e/block.test.ts index 9bdd6f65fb..dbb0f8079b 100644 --- a/packages/core/client/src/__tests__/e2e/block.test.ts +++ b/packages/core/client/src/__tests__/e2e/block.test.ts @@ -295,23 +295,23 @@ const tablePageSchema = { test.describe('add block & delete block', () => { test('add block,then delete block', async ({ page, mockPage }) => { await mockPage().goto(); - await page.getByLabel('schema-initializer-Grid-BlockInitializers').click(); - await page.getByLabel('dataBlocks-table', { exact: true }).click(); - await page.getByLabel('dataBlocks-table-Users').click(); + await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); + await page.getByRole('menuitem', { name: 'table Table' }).click(); + await page.getByRole('menuitem', { name: 'Users' }).click(); await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); await expect(page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users')).toBeVisible(); - await expect(await page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-users')).toBeVisible(); + await expect(page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-users')).toBeVisible(); //删除区块 await page.getByLabel('block-item-CardItem-users-table').hover(); await page .getByLabel('block-item-CardItem-users-table') .getByRole('button', { name: 'designer-schema-settings' }) .click(); - await page.getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByLabel('block-item-CardItem-users-table')).not.toBeVisible(); await expect(page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-users')).not.toBeVisible(); - await expect(await page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-users')).not.toBeVisible(); + await expect(page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-users')).not.toBeVisible(); }); }); @@ -320,17 +320,17 @@ test.describe('block title', () => { await mockPage({ pageSchema: tablePageSchema, }).goto(); - await page.getByLabel('block-item-CardItem-users-table').click(); + await page.getByLabel('block-item-CardItem-users-table').hover(); await page .getByLabel('block-item-CardItem-users-table') .getByRole('button', { name: 'designer-schema-settings' }) .hover(); - await page.getByLabel('Edit block title').click(); + await page.getByRole('menuitem', { name: 'Edit block title' }).click(); await page.getByLabel('block-item-Input-users-Block title').click(); await page.getByLabel('block-item-Input-users-Block title').getByRole('textbox').fill('block title'); - await page.locator('.ant-modal').getByRole('button', { name: 'OK' }).click(); + await page.locator('.ant-modal').getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByLabel('block-item-CardItem-users-table').locator('.ant-card-head')).toBeVisible(); - await expect(await page.getByLabel('block-item-CardItem-users-table').locator('.ant-card-head').innerText()).toBe( + await expect(page.getByLabel('block-item-CardItem-users-table').locator('.ant-card-head')).toHaveText( 'block title', ); @@ -338,15 +338,15 @@ test.describe('block title', () => { await page .getByLabel('block-item-CardItem-users-table') .getByRole('button', { name: 'designer-schema-settings' }) - .click(); - await page.getByText('Edit block title').click(); + .hover(); + await page.getByRole('menuitem', { name: 'Edit block title' }).click(); const inputValue = await page.getByRole('textbox').inputValue(); - await expect(inputValue).toBe('block title'); + expect(inputValue).toBe('block title'); }); }); -test.describe('blcok template', () => { - test('save block template', async ({ page, mockPage }) => { +test.describe('block template', () => { + test('save block template & using block template', async ({ page, mockPage }) => { await mockPage({ name: 'block template source', pageSchema: { @@ -494,61 +494,61 @@ test.describe('blcok template', () => { 'x-index': 1, }, }).goto(); - await page.getByLabel('block-item-CardItem-users-form').click(); + await page.getByLabel('block-item-CardItem-users-form').hover(); await page .getByLabel('block-item-CardItem-users-form') .getByLabel('designer-schema-settings-CardItem-FormV2.Designer-users') - .click(); - await page.getByLabel('Save as block template').click(); + .hover(); + await page.getByRole('menuitem', { name: 'Save as block template' }).click(); await page.getByLabel('Save as template').getByRole('textbox').fill('Users_Form'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByLabel('block-item-CardItem-users-form').hover(); - await page.waitForTimeout(1000); + //保存模板后当前区块为引用区块 const titleTag = await page.getByLabel('block-item-CardItem-users-form').locator('.title-tag').nth(1).innerText(); - await expect(titleTag).toContain('Reference template'); - }); - test('using block template ', async ({ page, mockPage }) => { + expect(titleTag).toContain('Reference template'); + + // using block template await mockPage({ name: 'block template', pageSchema: tablePageSchema, }).goto(); - await page.getByLabel('schema-initializer-Grid-BlockInitializers').click(); + await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); //使用复制模板 - await page.getByLabel('dataBlocks-form', { exact: true }).click(); - await page.getByLabel('dataBlocks-form-FormItem_table_subMenu').click(); - await page.getByRole('menuitem', { name: 'Duplicate template right' }).click(); - await page.getByText('Users_Form (Fields only)').first().click(); + await page.getByRole('menuitem', { name: 'form Form' }).first().hover(); + await page.getByRole('menuitem', { name: 'Users' }).hover(); + await page.getByRole('menuitem', { name: 'Duplicate template' }).hover(); + await page.getByRole('menuitem', { name: 'Users_Form (Fields only)' }).click(); await expect(page.getByLabel('block-item-CardItem-users-form')).toBeVisible(); //在新建操作中使用引用模板 await page.getByLabel('action-Action-Add new-create-users-table').click(); await page.getByLabel('schema-initializer-Grid-CreateFormBlockInitializers-users').click(); - await page.getByRole('menuitem', { name: 'form Form right' }).click(); - await page.getByRole('menuitem', { name: 'Reference template right' }).click(); - await page.getByRole('button', { name: 'Users_Form (Fields only)' }).first().click(); + await page.getByRole('menuitem', { name: 'form Form' }).hover(); + await page.getByRole('menuitem', { name: 'Reference template' }).hover(); + await page.getByRole('menuitem', { name: 'Users_Form (Fields only)' }).click(); await page.getByLabel('schema-initializer-Grid-CreateFormBlockInitializers-users').hover(); await expect(page.locator('.ant-drawer').getByLabel('block-item-CardItem-users-form')).toBeVisible(); await page.locator('.ant-drawer-mask').click(); //在编辑操作中使用引用模板 - await page.getByLabel('block-item-CardItem-users-table').getByLabel('Edit').click(); + await page.getByLabel('action-Action.Link-Edit-update-users-table-0').click(); await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-users').click(); - await page.getByRole('menuitem', { name: 'form Form right' }).click(); - await page.getByRole('menuitem', { name: 'Reference template right' }).click(); - await page.getByRole('button', { name: 'Users_Form (Fields only)' }).first().click(); + await page.getByRole('menuitem', { name: 'form Form' }).hover(); + await page.getByRole('menuitem', { name: 'Reference template' }).hover(); + await page.getByRole('menuitem', { name: 'Users_Form (Fields only)' }).click(); await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-users').hover(); //修改引用模板 await page.locator('.ant-drawer').getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Phone').click(); + await page.getByRole('menuitem', { name: 'Phone' }).click(); await page.locator('.ant-drawer-mask').click(); //复制模板不同步,引用模板同步 await expect( page.getByLabel('block-item-CardItem-users-form').getByLabel('block-item-CollectionField-users-form-users.phone'), ).not.toBeVisible(); await page.getByLabel('block-item-CardItem-users-table').getByLabel('action-Action-Add').click(); - await expect(await page.getByLabel('block-item-CollectionField-users-form-users.phone')).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-users-form-users.phone')).toBeVisible(); await page.locator('.ant-drawer-mask').click(); //删除模板 @@ -556,6 +556,6 @@ test.describe('blcok template', () => { await page.getByLabel('ui-schema-storage').click(); await page.getByRole('menuitem', { name: 'layout Block templates' }).click(); await page.getByLabel('action-Action.Link-Delete-destroy-uiSchemaTemplates-table-Users_Form').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); }); }); diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2165.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2165.test.ts index 446f8de42b..02adcaf3dd 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2165.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2165.test.ts @@ -285,7 +285,7 @@ test('BUG: variable labels should be displayed normally', async ({ page, mockPag await page.getByLabel('block-item-CardItem-users-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-users').hover(); - await page.getByLabel('Linkage rules').click(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await expect(page.getByText('Current form / Nickname')).toBeVisible(); await expect(page.getByText('Current form / Phone')).toBeVisible(); diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2174.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2174.test.ts index 509a1c89f0..c446bcab3d 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2174.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2174.test.ts @@ -6,6 +6,7 @@ const config = { version: '2.0', type: 'void', 'x-component': 'Page', + 'x-index': 1, properties: { x9ersztdq7x: { _isJSONSchemaObject: true, @@ -13,28 +14,31 @@ const config = { type: 'void', 'x-component': 'Grid', 'x-initializer': 'BlockInitializers', + 'x-index': 1, properties: { ppgwx2drpng: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Row', + 'x-index': 1, properties: { c25gfj884oe: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Col', + 'x-index': 1, properties: { urmc26btvb5: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-decorator': 'TableBlockProvider', - 'x-acl-action': 't_ylz5vtxncxq:list', + 'x-acl-action': 'test2174:list', 'x-decorator-props': { - collection: 't_ylz5vtxncxq', - resource: 't_ylz5vtxncxq', + collection: 'test2174', + resource: 'test2174', action: 'list', params: { pageSize: 20, @@ -47,6 +51,7 @@ const config = { 'x-designer': 'TableBlockDesigner', 'x-component': 'CardItem', 'x-filter-targets': [], + 'x-index': 1, properties: { actions: { _isJSONSchemaObject: true, @@ -59,9 +64,9 @@ const config = { marginBottom: 'var(--nb-spacing)', }, }, - 'x-uid': 'ixeu99rujou', - 'x-async': false, 'x-index': 1, + 'x-uid': '7wfu81uqxox', + 'x-async': false, }, yxgybgmfhkp: { _isJSONSchemaObject: true, @@ -76,6 +81,7 @@ const config = { }, useProps: '{{ useTableBlockProps }}', }, + 'x-index': 2, properties: { actions: { _isJSONSchemaObject: true, @@ -87,6 +93,7 @@ const config = { 'x-component': 'TableV2.Column', 'x-designer': 'TableV2.ActionColumnDesigner', 'x-initializer': 'TableActionColumnInitializers', + 'x-index': 1, properties: { actions: { _isJSONSchemaObject: true, @@ -97,22 +104,26 @@ const config = { 'x-component-props': { split: '|', }, + 'x-index': 1, properties: { r51kgwrhpgd: { + 'x-uid': 'udzm8hggmmb', _isJSONSchemaObject: true, version: '2.0', type: 'void', - title: '{{ t("View") }}', + title: 'View details', 'x-action': 'view', 'x-designer': 'Action.Designer', 'x-component': 'Action.Link', 'x-component-props': { openMode: 'drawer', + danger: false, }, 'x-decorator': 'ACLActionProvider', 'x-designer-props': { linkageAction: true, }, + 'x-index': 1, properties: { drawer: { _isJSONSchemaObject: true, @@ -123,6 +134,7 @@ const config = { 'x-component-props': { className: 'nb-action-popup', }, + 'x-index': 1, properties: { tabs: { _isJSONSchemaObject: true, @@ -131,6 +143,7 @@ const config = { 'x-component': 'Tabs', 'x-component-props': {}, 'x-initializer': 'TabPaneInitializers', + 'x-index': 1, properties: { tab1: { _isJSONSchemaObject: true, @@ -140,6 +153,7 @@ const config = { 'x-component': 'Tabs.TabPane', 'x-designer': 'Tabs.Designer', 'x-component-props': {}, + 'x-index': 1, properties: { grid: { _isJSONSchemaObject: true, @@ -147,18 +161,21 @@ const config = { type: 'void', 'x-component': 'Grid', 'x-initializer': 'RecordBlockInitializers', + 'x-index': 1, properties: { g8w7wq09bgo: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Row', + 'x-index': 1, properties: { '42t1ais26x8': { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Col', + 'x-index': 1, properties: { zmq6hmh2i3a: { _isJSONSchemaObject: true, @@ -167,19 +184,20 @@ const config = { 'x-acl-action-props': { skipScopeCheck: true, }, - 'x-acl-action': 't_ylz5vtxncxq.f_q32e4ieq49n:create', + 'x-acl-action': 'test2174.f_q32e4ieq49n:create', 'x-decorator': 'FormBlockProvider', 'x-decorator-props': { useSourceId: '{{ useSourceIdFromParentRecord }}', useParams: '{{ useParamsFromRecord }}', action: null, - resource: 't_ylz5vtxncxq.f_q32e4ieq49n', - collection: 't_ylz5vtxncxq', - association: 't_ylz5vtxncxq.f_q32e4ieq49n', + resource: 'test2174.f_q32e4ieq49n', + collection: 'test2174', + association: 'test2174.f_q32e4ieq49n', }, 'x-designer': 'FormV2.Designer', 'x-component': 'CardItem', 'x-component-props': {}, + 'x-index': 1, properties: { qii0gw1cb8e: { _isJSONSchemaObject: true, @@ -189,6 +207,7 @@ const config = { 'x-component-props': { useProps: '{{ useFormBlockProps }}', }, + 'x-index': 1, properties: { grid: { _isJSONSchemaObject: true, @@ -196,20 +215,23 @@ const config = { type: 'void', 'x-component': 'Grid', 'x-initializer': 'FormItemInitializers', + 'x-index': 1, properties: { s3hhb0ohnv1: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Row', + 'x-index': 1, properties: { t2qtgv250s0: { _isJSONSchemaObject: true, version: '2.0', type: 'void', 'x-component': 'Grid.Col', + 'x-index': 1, properties: { - f_nr8xi7ezw5t: { + singleSelect: { _isJSONSchemaObject: true, version: '2.0', type: 'string', @@ -217,30 +239,27 @@ const config = { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': - 't_ylz5vtxncxq.f_nr8xi7ezw5t', + 'test2174.singleSelect', 'x-component-props': { style: { width: '100%', }, }, - 'x-uid': 'gr6ye0ejgkj', - 'x-async': false, 'x-index': 1, + 'x-uid': '75ls3qlq3kw', + 'x-async': false, }, }, - 'x-uid': 'jz51fg61juf', + 'x-uid': 'ndb8zhrxgq0', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'dw9potxn5az', + 'x-uid': '14q94mijyx4', 'x-async': false, - 'x-index': 2, }, }, - 'x-uid': 'vyprewc6op9', + 'x-uid': '6vfyrsrkw29', 'x-async': false, - 'x-index': 1, }, actions: { _isJSONSchemaObject: true, @@ -254,6 +273,7 @@ const config = { marginTop: 24, }, }, + 'x-index': 2, properties: { afuxokt3osc: { _isJSONSchemaObject: true, @@ -271,69 +291,56 @@ const config = { triggerWorkflows: [], }, type: 'void', - 'x-uid': '5fz8e6oboog', - 'x-async': false, 'x-index': 1, + 'x-uid': '6idd8u0fs5v', + 'x-async': false, }, }, - 'x-uid': 'up7fy4jbynz', + 'x-uid': '8xm66k2wcbq', 'x-async': false, - 'x-index': 2, }, }, - 'x-uid': 'rts053txv5d', + 'x-uid': 'ia7qdocnj0k', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': '84cihdbgsy4', + 'x-uid': 'ai78ycshc04', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'e58k99p8v7t', + 'x-uid': 'gipnqnxa7c7', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'glf52r13dz6', + 'x-uid': '4fztm3s6pqr', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': '14rfeq12sgv', + 'x-uid': 'yiwh3chdjzg', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': '6j7qo163w0u', + 'x-uid': 'mar9ww8zdd6', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'nzslc0b3a5a', + 'x-uid': '29brl1obftp', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'umv8rca71ox', + 'x-uid': 'r4qbds05tcz', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'dormxy03out', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 's5hys18b8r1', + 'x-uid': 'hp7glbaipmc', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'e5641icd1oh', + 'x-uid': 'k9u1fta4jdq', 'x-async': false, - 'x-index': 1, }, x6soi4xw3yn: { _isJSONSchemaObject: true, @@ -342,12 +349,12 @@ const config = { 'x-decorator': 'TableV2.Column.Decorator', 'x-designer': 'TableV2.Column.Designer', 'x-component': 'TableV2.Column', + 'x-index': 2, properties: { - f_nr8xi7ezw5t: { - 'x-uid': 'if1clbvakzu', + singleSelect: { _isJSONSchemaObject: true, version: '2.0', - 'x-collection-field': 't_ylz5vtxncxq.f_nr8xi7ezw5t', + 'x-collection-field': 'test2174.singleSelect', 'x-component': 'CollectionField', 'x-component-props': { style: { @@ -363,13 +370,13 @@ const config = { }, }, default: 'np55dbbny0e', - 'x-async': false, 'x-index': 1, + 'x-uid': 'ots1wvv436n', + 'x-async': false, }, }, - 'x-uid': '46jf982acnq', + 'x-uid': 'yx4mdriq3jp', 'x-async': false, - 'x-index': 2, }, aoj0myt8kgn: { _isJSONSchemaObject: true, @@ -378,18 +385,18 @@ const config = { 'x-decorator': 'TableV2.Column.Decorator', 'x-designer': 'TableV2.Column.Designer', 'x-component': 'TableV2.Column', + 'x-index': 3, properties: { f_q32e4ieq49n: { - 'x-uid': 'nmevvkp1dyq', _isJSONSchemaObject: true, version: '2.0', - 'x-collection-field': 't_ylz5vtxncxq.f_q32e4ieq49n', + 'x-collection-field': 'test2174.f_q32e4ieq49n', 'x-component': 'CollectionField', 'x-component-props': { ellipsis: true, size: 'small', fieldNames: { - label: 'f_nr8xi7ezw5t', + label: 'singleSelect', value: 'id', }, }, @@ -400,6 +407,7 @@ const config = { display: 'none', }, }, + 'x-index': 1, properties: { ony7hpl5247: { _isJSONSchemaObject: true, @@ -419,6 +427,7 @@ const config = { 'x-component': 'Tabs', 'x-component-props': {}, 'x-initializer': 'TabPaneInitializers', + 'x-index': 1, properties: { tab1: { _isJSONSchemaObject: true, @@ -428,6 +437,7 @@ const config = { 'x-component': 'Tabs.TabPane', 'x-designer': 'Tabs.Designer', 'x-component-props': {}, + 'x-index': 1, properties: { grid: { _isJSONSchemaObject: true, @@ -435,78 +445,83 @@ const config = { type: 'void', 'x-component': 'Grid', 'x-initializer': 'RecordBlockInitializers', - 'x-uid': 'z8chebednow', - 'x-async': false, 'x-index': 1, + 'x-uid': 'roog7uz0d2o', + 'x-async': false, }, }, - 'x-uid': 'bemxws6nlsp', + 'x-uid': 'x05lq9x1n0n', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'kkiviyd3hax', + 'x-uid': 'hrymp5hthg3', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'g2ynjolpxxr', + 'x-uid': 'ujkf9096qwq', 'x-async': false, }, }, + 'x-uid': '02kpdjz33yd', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'mxhts7v66xt', + 'x-uid': 'e366r5cz2cv', 'x-async': false, - 'x-index': 3, }, }, - 'x-uid': 'jvbe1vvv2x5', + 'x-uid': 'wkdjjugn3l8', 'x-async': false, - 'x-index': 2, }, }, - 'x-uid': 'q8igo0fwc04', + 'x-uid': 'n3f07077krt', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'bxeh7r9vao9', + 'x-uid': 'i920mrykzkp', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': '2kiklqcijua', + 'x-uid': '8a5n45kb55h', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'pyrahf1c66i', + 'x-uid': 'wcobb00rn5y', 'x-async': false, - 'x-index': 1, }, }, - 'x-uid': 'u9vjrr5ehhq', + 'x-uid': 'pfr9jap0zsf', 'x-async': true, - 'x-index': 1, }, collections: [ { - name: 't_ylz5vtxncxq', + name: 'test2174', title: 'Test', fields: [ { name: 'f_lkqy3eh4ag7', interface: 'integer', + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_lkqy3eh4ag7', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, }, { name: 'f_rathx54cqpy', interface: 'integer', + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_rathx54cqpy', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, }, { - name: 'f_nr8xi7ezw5t', + name: 'singleSelect', interface: 'select', uiSchema: { enum: [ @@ -543,7 +558,9 @@ const config = { }, title: 'One to many', }, - target: 't_ylz5vtxncxq', + target: 'test2174', + targetKey: 'id', + sourceKey: 'id', }, ], }, @@ -551,18 +568,16 @@ const config = { }; // fix https://nocobase.height.app/T-2174 -test.skip('BUG: should show default value option', async ({ page, mockPage, mockRecord }) => { +test('BUG: should show default value option', async ({ page, mockPage, mockRecord }) => { const nocoPage = await mockPage(config).waitForInit(); - await mockRecord('t_ylz5vtxncxq'); + await mockRecord('test2174'); await nocoPage.goto(); - await page.getByLabel('action-Action.Link-View-view-t_ylz5vtxncxq-table-0').click(); + await page.getByLabel('action-Action.Link-View details-view-test2174-table-0').click(); + await page.getByLabel('block-item-CollectionField-test2174-form-test2174.singleSelect-Single select').hover(); await page - .getByLabel('block-item-CollectionField-t_ylz5vtxncxq-form-t_ylz5vtxncxq.f_nr8xi7ezw5t-Single select') - .hover(); - await page - .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-t_ylz5vtxncxq-t_ylz5vtxncxq.f_nr8xi7ezw5t') + .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-test2174-test2174.singleSelect') .hover(); - await expect(page.getByLabel('Set default value')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).toBeVisible(); }); diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2176.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2176.test.ts index 24c7850f63..acf7584614 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2176.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2176.test.ts @@ -218,6 +218,8 @@ test('BUG: Relationship block issue, menu should not display loading', async ({ await mockPage(config).goto(); await page.getByLabel('action-Action.Link-View-view-roles-table-root').click(); await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-roles').hover(); + + // 因为下面的断言是判断是否隐藏,又因为一开始就是隐藏的,所以为了准确,需要等待一下 await page.waitForTimeout(300); // 这个输入框不应该显示 diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2183.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2183.test.ts index f4d5249474..88940d2580 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2183.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2183.test.ts @@ -165,7 +165,7 @@ test('BUG: should save conditions', async ({ page, mockPage }) => { await mockPage(config).goto(); await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByTestId('filter-select-field').getByLabel('Search').click(); + await page.getByTestId('select-filter-field').getByLabel('Search').click(); await page.getByRole('menuitemcheckbox', { name: 'ID' }).click(); await page.getByRole('button', { name: 'Save conditions' }).click(); @@ -173,6 +173,6 @@ test('BUG: should save conditions', async ({ page, mockPage }) => { await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click(); // After refreshing the browser, the set field and operator should still be visible - await expect(page.getByTestId('filter-select-field').getByText('ID')).toBeVisible(); - await expect(page.getByTestId('filter-select-operator').getByText('is')).toBeVisible(); + await expect(page.getByTestId('select-filter-field').getByText('ID')).toBeVisible(); + await expect(page.getByTestId('select-filter-operator').getByText('is')).toBeVisible(); }); diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2186.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2186.test.ts index b8acdb9466..3ca8acdefa 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2186.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2186.test.ts @@ -165,7 +165,7 @@ test('BUG(Filter): the input box displayed should correspond to the field type', await mockPage(config).goto(); await page.getByLabel('action-Filter.Action-Filter-filter-users-table').click(); - await page.getByTestId('filter-select-field').getByLabel('Search').click(); + await page.getByTestId('select-filter-field').getByLabel('Search').click(); await page.getByRole('menuitemcheckbox', { name: 'ID' }).click(); // 应该显示数字输入框 diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts index 44acd3410b..d293e2912d 100644 --- a/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts @@ -359,7 +359,7 @@ test('BUG: should be possible to change the value of the association field norma await expect(page.getByLabel('Root')).toBeVisible(); await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover(); - await page.getByLabel('Display collection fields-Nickname').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.mouse.move(200, 0); diff --git a/packages/core/client/src/__tests__/e2e/createCollections.test.ts b/packages/core/client/src/__tests__/e2e/createCollections.test.ts index 9a5d79fee9..2357feb53c 100644 --- a/packages/core/client/src/__tests__/e2e/createCollections.test.ts +++ b/packages/core/client/src/__tests__/e2e/createCollections.test.ts @@ -248,10 +248,10 @@ test.describe('createCollections', () => { await mockPage(pageConfig).goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-table', { exact: true }).hover(); + await page.getByRole('menuitem', { name: 'table Table' }).hover(); - await expect(page.getByLabel('dataBlocks-table-collection1')).toBeVisible(); - await expect(page.getByLabel('dataBlocks-table-collection2')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'collection1' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'collection2' })).toBeVisible(); }); test('no page, just create collections', async ({ page, createCollections }) => { diff --git a/packages/core/client/src/__tests__/e2e/createDataBlock.test.ts b/packages/core/client/src/__tests__/e2e/createDataBlock.test.ts index 1264e0c00b..2b515f3ce6 100644 --- a/packages/core/client/src/__tests__/e2e/createDataBlock.test.ts +++ b/packages/core/client/src/__tests__/e2e/createDataBlock.test.ts @@ -5,8 +5,8 @@ test.describe('create data block', () => { await mockPage().goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-table', { exact: true }).hover(); - await page.getByLabel('dataBlocks-table-Users').click(); + await page.getByRole('menuitem', { name: 'table Table' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); await expect(page.getByText('Configure columns')).toBeVisible(); }); @@ -15,8 +15,9 @@ test.describe('create data block', () => { await mockPage().goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-form', { exact: true }).hover(); - await page.getByLabel('dataBlocks-form-Users').click(); + await page.getByRole('menuitem', { name: 'Form' }).first().hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.mouse.move(300, 0); await expect(page.getByText('Configure fields')).toBeVisible(); }); @@ -25,8 +26,8 @@ test.describe('create data block', () => { await mockPage().goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-details', { exact: true }).hover(); - await page.getByLabel('dataBlocks-details-Users').click(); + await page.getByRole('menuitem', { name: 'Details' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); await expect(page.getByText('Configure fields')).toBeVisible(); }); @@ -35,8 +36,8 @@ test.describe('create data block', () => { await mockPage().goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-List', { exact: true }).hover(); - await page.getByLabel('dataBlocks-List-Users').click(); + await page.getByRole('menuitem', { name: 'ordered-list List' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); await expect(page.getByText('Configure fields').first()).toBeVisible(); }); @@ -45,8 +46,8 @@ test.describe('create data block', () => { await mockPage().goto(); await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); - await page.getByLabel('dataBlocks-GridCard', { exact: true }).hover(); - await page.getByLabel('dataBlocks-GridCard-Users').click(); + await page.getByRole('menuitem', { name: 'Grid Card' }).hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); await expect(page.getByText('Configure fields').first()).toBeVisible(); }); diff --git a/packages/core/client/src/__tests__/e2e/field.test.ts b/packages/core/client/src/__tests__/e2e/field.test.ts index b5f0ee4559..bf4c89b9c2 100644 --- a/packages/core/client/src/__tests__/e2e/field.test.ts +++ b/packages/core/client/src/__tests__/e2e/field.test.ts @@ -111,76 +111,70 @@ const formPageSchema = { test.describe('add field & remove field in block', () => { test('add field,then remove field in block', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); - await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Nickname').click(); - await page.getByLabel('Display collection fields-Username').click(); - await page.getByLabel('Display collection fields-Email').click(); + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + await page.getByRole('menuitem', { name: 'Email' }).click(); //添加字段 await expect(page.getByLabel('block-item-CollectionField-users-form-users.nickname')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-users-form-users.username')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-users-form-users.email')).toBeVisible(); //激活的状态 - await expect(await page.getByLabel('Display collection fields-Nickname').getByRole('switch').isChecked()).toBe( - true, - ); - await expect(await page.getByLabel('Display collection fields-Username').getByRole('switch').isChecked()).toBe( - true, - ); - await expect(await page.getByLabel('Display collection fields-Email').getByRole('switch').isChecked()).toBe(true); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Username' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Email' }).getByRole('switch')).toBeChecked(); //移除字段 - await page.getByLabel('Display collection fields-Nickname').click(); - await page.getByLabel('Display collection fields-Username').click(); - await page.getByLabel('Display collection fields-Email').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + await page.getByRole('menuitem', { name: 'Email' }).click(); await expect(page.getByLabel('block-item-CollectionField-users-form-users.nickname')).not.toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-users-form-users.username')).not.toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-users-form-users.email')).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Username' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Email' }).getByRole('switch')).not.toBeChecked(); }); }); test.describe('drag field in block', () => { - test.skip('drag field for layout', async ({ page, mockPage }) => { + test('drag field for layout', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Nickname').click(); - await page.getByLabel('Display collection fields-Username').click(); - await page.getByLabel('Display collection fields-Email').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + await page.getByRole('menuitem', { name: 'Email' }).click(); - const sourceElement = await page.getByLabel('block-item-CollectionField-users-form-users.nickname'); + const sourceElement = page.getByLabel('block-item-CollectionField-users-form-users.nickname'); await sourceElement.hover(); - const source = await page - .getByLabel('block-item-CollectionField-users-form-users.nickname') - .getByLabel('designer-drag'); + const source = sourceElement.getByLabel('designer-drag'); await source.hover(); - - const targetElement = await page.getByLabel('block-item-CollectionField-users-form-users.username'); + const targetElement = page.getByLabel('block-item-CollectionField-users-form-users.username'); await source.dragTo(targetElement); - const targetElement2 = await page.getByLabel('block-item-CollectionField-users-form-users.email'); - await page.getByLabel('block-item-CollectionField-users-form-users.nickname').getByLabel('designer-drag').hover(); - await page - .getByLabel('block-item-CollectionField-users-form-users.nickname') - .getByLabel('designer-drag') - .dragTo(targetElement2); + const targetElement2 = page.getByLabel('block-item-CollectionField-users-form-users.email'); + await source.hover(); + await source.dragTo(targetElement2); + + await sourceElement.hover(); const nickname = await source.boundingBox(); const username = await targetElement.boundingBox(); const email = await targetElement2.boundingBox(); const max = Math.max(username.y, nickname.y, email.y); //拖拽调整排序符合预期 - await expect(nickname.y).toBe(max); + expect(nickname.y).toBe(max); }); }); test.describe('field setting config ', () => { test('edit field label in block', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); - await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Username').click(); + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover(); + await page.getByRole('menuitem', { name: 'Username' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.username').hover(); - await page.getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-users-users.username').click(); - // await page.getByLabel('block-item-CollectionField-users-form-users.username').getByLabel('designer-schema-settings').click(); - await page.getByLabel('Edit field title').click(); + await page.getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-users-users.username').hover(); + await page.getByRole('menuitem', { name: 'Edit field title' }).click(); await page.getByLabel('block-item-Input-users-Field title').getByRole('textbox').fill('Username1'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect( page.getByLabel('block-item-CollectionField-users-form-users.username').getByText('Username1'), ).toBeVisible(); @@ -190,26 +184,23 @@ test.describe('field setting config ', () => { .getByLabel('block-item-CollectionField-users-form-users.username') .getByLabel('designer-schema-settings') .hover(); - await page.getByLabel('Edit field title').click(); - await page.waitForTimeout(1000); // 等待1秒钟 - await expect(await page.getByLabel('block-item-Input-users-Field title').getByRole('textbox').inputValue()).toBe( - 'Username1', - ); + await page.getByRole('menuitem', { name: 'Edit field title' }).click(); + await expect(page.getByLabel('block-item-Input-users-Field title').getByRole('textbox')).toHaveValue('Username1'); }); test('display & not display field label in block ', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); - await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Username').click(); + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover(); + await page.getByRole('menuitem', { name: 'Username' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.username').click(); await page .getByLabel('block-item-CollectionField-users-form-users.username') .getByLabel('designer-schema-settings') .hover(); - await page.getByLabel('Display title').hover(); + await page.getByRole('menuitem', { name: 'Display title' }).hover(); //默认显示 - await expect(await page.getByLabel('Display title').getByRole('switch').isChecked()).toBe(true); + await expect(page.getByRole('menuitem', { name: 'Display title' }).getByRole('switch')).toBeChecked(); //设置不显示标题 - await page.getByLabel('Display title').click(); + await page.getByRole('menuitem', { name: 'Display title' }).click(); const labelItem = page .getByLabel('block-item-CollectionField-users-form-users.username') .locator('.ant-formily-item-label'); @@ -224,7 +215,7 @@ test.describe('field setting config ', () => { .getByLabel('block-item-CollectionField-users-form-users.username') .getByLabel('designer-schema-settings') .hover(); - await page.getByLabel('Display title').click(); + await page.getByRole('menuitem', { name: 'Display title' }).click(); const labelDisplay1 = await labelItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return computedStyle.display; @@ -236,16 +227,16 @@ test.describe('field setting config ', () => { const description = 'field description'; const descriptionColor = 'rgba(0, 0, 0, 0.65)'; await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Username').click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.username').click(); await page .getByLabel('block-item-CollectionField-users-form-users.username') .getByLabel('designer-schema-settings') .hover(); - await page.getByLabel('Edit description').click(); + await page.getByRole('menuitem', { name: 'Edit description' }).click(); await page.getByLabel('block-item-Input.TextArea-users').locator('textarea').fill(description); - await page.getByRole('button', { name: 'OK' }).click(); - const descriptionItem = await page + await page.getByRole('button', { name: 'OK', exact: true }).click(); + const descriptionItem = page .getByLabel('block-item-CollectionField-users-form-users.username') .locator('.ant-formily-item-extra'); const descriptionItemColor = await descriptionItem.evaluate((element) => { @@ -255,27 +246,24 @@ test.describe('field setting config ', () => { //字段描述样式符合预期 expect(await descriptionItem.innerText()).toBe(description); const isApproximate = approximateColor(descriptionItemColor, descriptionColor); - await expect(isApproximate).toBe(true); + expect(isApproximate).toBe(true); }); test('field required ', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Nickname').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.nickname').click(); await page .getByLabel('block-item-CollectionField-users-form-users.nickname') .getByLabel('designer-schema-settings') - .click(); - await page.getByLabel('Required').click(); + .hover(); + await page.getByRole('menuitem', { name: 'Required' }).click(); await page.getByLabel('schema-initializer-ActionBar-FormActionInitializers-users').click(); - await page.getByLabel('Enable actions-Submit').click(); + await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.getByLabel('action-Action-Submit-submit-users-form').click(); - await page.waitForTimeout(1000); //必填校验符合预期 await expect( - await page - .getByLabel('block-item-CollectionField-users-form-users.nickname') - .locator('.ant-formily-item-error-help'), + page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('.ant-formily-item-error-help'), ).toBeVisible(); const inputItem = page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('input'); const inputErrorBorderColor = await inputItem.evaluate((element) => { @@ -284,66 +272,61 @@ test.describe('field setting config ', () => { }); //样式符合预期 expect(inputErrorBorderColor).toBe('rgb(255, 77, 79)'); + // TODO: 该断言无效,因为在任何情况下该断言都能通过 // 断言表单未被提交 - expect(await page.getByLabel('block-item-CardItem-users-form').locator('form')).not.toHaveProperty( - 'submitted', - true, - ); + expect(page.getByLabel('block-item-CardItem-users-form').locator('form')).not.toHaveProperty('submitted', true); }); test('field validation rule ', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); const errorMessage = 'this is error message'; await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Nickname').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.nickname').click(); await page .getByLabel('block-item-CollectionField-users-form-users.nickname') .getByLabel('designer-schema-settings') - .click(); - await page.getByText('Set validation rules').click(); + .hover(); + await page.getByRole('menuitem', { name: 'Set validation rules' }).click(); await page.getByRole('button', { name: 'plus Add validation rule' }).click(); - await await page.getByLabel('block-item-InputNumber-users-Max length').getByRole('spinbutton').fill('3'); + await page.getByLabel('block-item-InputNumber-users-Max length').getByRole('spinbutton').fill('3'); await page.getByRole('button', { name: 'Error message' }).locator('textarea').fill(errorMessage); await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.nickname').getByRole('textbox').fill('1111'); - const errorInfo = await page + const errorInfo = page .getByLabel('block-item-CollectionField-users-form-users.nickname') .locator('.ant-formily-item-error-help'); await expect(errorInfo).toBeVisible(); //报错信息符合预期 - await expect(await errorInfo.innerText()).toBe(errorMessage); + expect(await errorInfo.innerText()).toBe(errorMessage); await page.getByLabel('schema-initializer-ActionBar-FormActionInitializers-users').click(); - await page.getByLabel('Enable actions-Submit').click(); + await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.getByLabel('action-Action-Submit-submit-users-form').click(); - await page.waitForTimeout(1000); + // TODO: 该断言无效,因为在任何情况下该断言都能通过 // 断言表单未被提交 - expect(await page.getByLabel('block-item-CardItem-users-form').locator('form')).not.toHaveProperty( - 'submitted', - true, - ); + expect(page.getByLabel('block-item-CardItem-users-form').locator('form')).not.toHaveProperty('submitted', true); }); test('field pattern ', async ({ page, mockPage }) => { await mockPage({ pageSchema: formPageSchema }).goto(); await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').click(); - await page.getByLabel('Display collection fields-Nickname').click(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); await page.getByLabel('block-item-CollectionField-users-form-users.nickname').click(); - const inputElement = await page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('input'); + const inputElement = page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('input'); await page .getByLabel('block-item-CollectionField-users-form-users.nickname') .getByLabel('designer-schema-settings') - .click(); - await page.getByLabel('Pattern').click(); + .hover(); + await page.getByRole('menuitem', { name: 'Pattern' }).click(); //禁用 await page.getByRole('option', { name: 'Readonly' }).click(); - await expect(await inputElement.isDisabled()).toBe(true); - await page.getByLabel('Pattern').click(); + await expect(inputElement).toBeDisabled(); + await page.getByRole('menuitem', { name: 'Pattern' }).click(); //只读 await page.getByText('Easy-reading').click(); await expect( - await page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('.ant-description-input'), + page.getByLabel('block-item-CollectionField-users-form-users.nickname').locator('.ant-description-input'), ).toBeInViewport(); }); }); diff --git a/packages/core/client/src/__tests__/e2e/menu.test.ts b/packages/core/client/src/__tests__/e2e/menu.test.ts index f81db165bb..92444ac8be 100644 --- a/packages/core/client/src/__tests__/e2e/menu.test.ts +++ b/packages/core/client/src/__tests__/e2e/menu.test.ts @@ -5,12 +5,11 @@ test.describe('menu page', () => { test('create new page, then delete', async ({ page, mockPage }) => { await mockPage().goto(); const pageTitle = 'new page'; - await page.getByLabel('schema-initializer-Menu-header').click(); - await page.waitForTimeout(1000); // 等待1秒钟 - await page.getByRole('menu').getByLabel('Page').click(); + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Page' }).click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill(pageTitle); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); const menuItem = page.getByRole('menu').locator('li').filter({ hasText: pageTitle }); const defaultBackgroundColor = await menuItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); @@ -18,46 +17,46 @@ test.describe('menu page', () => { }); await menuItem.click(); // 获取激活后的背景高亮颜色 - const activedBackgroundColor = await menuItem.evaluate((element) => { + const activeBackgroundColor = await menuItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return computedStyle.backgroundColor; }); // 菜单高亮/进入空页面只有一个add block 按钮 - expect(activedBackgroundColor).not.toBe(defaultBackgroundColor); + expect(activeBackgroundColor).not.toBe(defaultBackgroundColor); await expect(page.getByLabel('schema-initializer-Grid-BlockInitializers')).toBeVisible(); await expect(page.getByTitle(pageTitle)).toBeVisible(); - const divElement = await page.locator('.nb-page-content'); + const divElement = page.locator('.nb-page-content'); const buttons = await divElement.locator('button').count(); const divText = await divElement.textContent(); - await expect(buttons).toEqual(1); - await expect(divText).toBe('Add block'); + expect(buttons).toEqual(1); + expect(divText).toBe('Add block'); // 删除页面,避免影响其他测试 await page.getByLabel(pageTitle).click(); await page.getByLabel(pageTitle).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await mockPage().goto(); await expect(page.getByTitle(pageTitle)).not.toBeVisible(); }); test('edit menu title', async ({ page, mockPage }) => { const pageTitle = 'page title'; - const newPagetitle = 'page title1'; + const newPageTitle = 'page title1'; await mockPage({ name: pageTitle, }).goto(); await page.getByLabel(pageTitle).hover(); await page.getByLabel(pageTitle).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Edit').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.mouse.move(300, 0); await page.getByRole('textbox').click(); - await page.getByRole('textbox').fill(newPagetitle); - await page.getByRole('button', { name: 'OK' }).click(); - await page.getByRole('menu').getByText(newPagetitle).click(); + await page.getByRole('textbox').fill(newPageTitle); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('menu').getByText(newPageTitle).click(); await mockPage().goto(); - await page.getByText(newPagetitle).click(); - await expect(page.getByTitle(newPagetitle)).toBeVisible(); + await page.getByText(newPageTitle).click(); + await expect(page.getByTitle(newPageTitle)).toBeVisible(); }); test('move menu ', async ({ page, mockPage }) => { const pageTitle1 = 'page1'; @@ -70,12 +69,11 @@ test.describe('menu page', () => { await page.getByRole('menu').getByText(pageTitle1).click(); await page.getByRole('menu').getByText(pageTitle1).hover(); await page.getByLabel(pageTitle1).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Move to').click(); + await page.getByRole('menuitem', { name: 'Move to' }).click(); await page.getByRole('dialog').click(); await page.getByLabel('Search').click(); - await page.waitForTimeout(1000); // 等待1秒钟 await page.locator('.ant-select-dropdown').getByText(pageTitle2).click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); const page1 = await page.getByRole('menu').getByText(pageTitle1).boundingBox(); const page2 = await page.getByRole('menu').getByText(pageTitle2).boundingBox(); //拖拽菜单排序符合预期 @@ -93,11 +91,11 @@ test.describe('menu page', () => { await page.getByLabel(pageTitle3).hover(); await page.getByLabel(pageTitle3).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Insert before').hover(); + await page.getByRole('menuitem', { name: 'Insert before' }).hover(); - await page.getByRole('button', { name: 'Page', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').fill(pageTitle4); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); const page3 = await page.getByLabel(pageTitle3).boundingBox(); const page4 = await page.getByLabel(pageTitle4).boundingBox(); @@ -108,8 +106,8 @@ test.describe('menu page', () => { //删除页面 await page.getByLabel(pageTitle4).click(); await page.getByLabel(pageTitle4).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); }); test('insert page after', async ({ page, mockPage }) => { @@ -123,11 +121,11 @@ test.describe('menu page', () => { await page.getByLabel(pageTitle5).hover(); await page.getByLabel(pageTitle5).getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Insert after').hover(); + await page.getByRole('menuitem', { name: 'Insert after' }).hover(); - await page.getByRole('button', { name: 'Page', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').fill(pageTitle6); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); const page6 = await page.getByLabel(pageTitle6).boundingBox(); const page5 = await page.getByLabel(pageTitle5).boundingBox(); @@ -136,23 +134,23 @@ test.describe('menu page', () => { await page.getByLabel(pageTitle6).click(); await expect(page.getByLabel('schema-initializer-Grid-BlockInitializers')).toBeVisible(); //删除页面 - await page.getByLabel(pageTitle6).getByLabel('designer-schema-settings-Menu.Item').click(); - await page.getByRole('menu').getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByLabel(pageTitle6).getByLabel('designer-schema-settings-Menu.Item').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); }); }); test.describe('menu group', () => { test('create new menu group, then delete', async ({ page, mockPage }) => { await mockPage().goto(); - await page.getByLabel('schema-initializer-Menu-header').hover(); - await page.getByRole('menu').getByLabel('Group').click(); + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Group' }).click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('menu Group'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByText('menu Group').click(); - await expect(page.getByLabel('schema-initializer-Menu-side')).toBeVisible(); + await expect(page.getByTestId('schema-initializer-Menu-side')).toBeVisible(); const sideBar = await page.locator('ul').filter({ hasText: /^Add menu item$/ }); await expect(sideBar).toBeVisible(); @@ -161,11 +159,11 @@ test.describe('menu group', () => { .locator('ul') .filter({ hasText: /^Add menu item$/ }) .click(); - await page.getByLabel('schema-initializer-Menu-side').click(); - await page.getByRole('button', { name: 'Page' }).click(); + await page.getByTestId('schema-initializer-Menu-side').click(); + await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('group page'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByText('group page').click(); //进入子页面 await expect(page.getByTitle('group page')).toBeVisible(); @@ -175,45 +173,40 @@ test.describe('menu group', () => { const computedStyle = window.getComputedStyle(element); return computedStyle.backgroundColor; }); - await page.waitForTimeout(1000); // 等待1秒钟 const isApproximate = approximateColor(menuItemBackgroundColor, 'rgba(255, 255, 255, 0.1)'); - await expect(isApproximate).toBe(true); - const pageItem = await page.getByRole('menu').locator('li').filter({ hasText: 'group page' }); + expect(isApproximate).toBe(true); + const pageItem = page.getByRole('menu').locator('li').filter({ hasText: 'group page' }); const pageItemBackgroundColor = await pageItem.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return computedStyle.backgroundColor; }); - await expect(pageItemBackgroundColor).toBe('rgb(230, 244, 255)'); + expect(pageItemBackgroundColor).toBe('rgb(230, 244, 255)'); // 删除页面,避免影响其他测试 await page.getByLabel('menu Group').click(); await page.getByLabel('menu Group').hover(); await page.getByLabel('menu Group').getByLabel('designer-schema-settings').first().hover(); - await page.getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await mockPage().goto(); await expect(page.getByTitle('menu Group')).not.toBeVisible(); }); }); test.describe('menu link', () => { - test('create new menu link, then delete', async ({ page, mockPage }) => { + test('create new menu link, then delete', async ({ page, mockPage, deletePage }) => { await mockPage().goto(); - await page.getByLabel('schema-initializer-Menu-header').hover(); - await page.getByRole('menu').getByLabel('Link').click(); + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Link' }).click(); await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill('link menu'); await page.getByLabel('block-item-Input-Link').getByRole('textbox').fill('https://www.baidu.com/'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByLabel('link menu').click(); - const page2Promise = page.waitForEvent('popup'); - const page2 = await page2Promise; + const page2 = await page.waitForEvent('popup'); - await expect(page2.getByRole('button', { name: '百度一下' })).toBeVisible(); + expect(page2.url()).toBe('https://www.baidu.com/'); // 删除页面,避免影响其他测试 - await page.getByLabel('link menu').click(); - await page.getByLabel('link menu').getByLabel('designer-schema-settings').hover(); - await page.getByLabel('Delete').click(); - await page.getByRole('button', { name: 'OK' }).click(); + await deletePage('link menu'); }); }); diff --git a/packages/core/client/src/__tests__/e2e/page.test.ts b/packages/core/client/src/__tests__/e2e/page.test.ts index ae2ff48459..dc02784657 100644 --- a/packages/core/client/src/__tests__/e2e/page.test.ts +++ b/packages/core/client/src/__tests__/e2e/page.test.ts @@ -8,16 +8,16 @@ test.describe('page header', () => { await expect(page.getByTitle(pageTitle)).toBeVisible(); await page.getByTitle(pageTitle).click(); await page.getByLabel('designer-schema-settings-Page').hover(); - await expect(page.getByLabel('Enable page header').getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Enable page header' }).getByRole('switch')).toBeChecked(); //关闭 - await page.getByLabel('Enable page header').getByRole('switch').click(); - await expect(await page.locator('.ant-page-header')).not.toBeVisible(); - await expect(page.getByLabel('Enable page header').getByRole('switch')).not.toBeChecked(); + await page.getByRole('menuitem', { name: 'Enable page header' }).getByRole('switch').click(); + await expect(page.locator('.ant-page-header')).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Enable page header' }).getByRole('switch')).not.toBeChecked(); //开启 await page.getByRole('main').locator('span').nth(1).click(); await page.getByLabel('designer-schema-settings-Page').hover(); - await page.getByLabel('Enable page header').getByRole('switch').click(); - await expect(await page.locator('.ant-page-header').getByTitle(pageTitle)).toBeVisible(); + await page.getByRole('menuitem', { name: 'Enable page header' }).getByRole('switch').click(); + await expect(page.locator('.ant-page-header').getByTitle(pageTitle)).toBeVisible(); }); }); @@ -29,16 +29,16 @@ test.describe('page title', () => { await expect(page.getByTitle(pageTitle)).toBeVisible(); await page.getByTitle(pageTitle).click(); await page.getByLabel('designer-schema-settings-Page').hover(); - await expect(page.getByLabel('Display page title').getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Display page title' }).getByRole('switch')).toBeChecked(); //不显示 - await page.getByLabel('Display page title').getByRole('switch').click(); + await page.getByRole('menuitem', { name: 'Display page title' }).getByRole('switch').click(); await expect(page.locator('.ant-page-header')).toBeVisible(); await expect(page.locator('.ant-page-header').getByTitle(pageTitle)).not.toBeVisible(); - await expect(page.getByLabel('Display page title').getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Display page title' }).getByRole('switch')).not.toBeChecked(); //开启 await page.locator('.ant-page-header').click(); await page.getByLabel('designer-schema-settings-Page').hover(); - await page.getByLabel('Display page title').getByRole('switch').click(); + await page.getByRole('menuitem', { name: 'Display page title' }).getByRole('switch').click(); await expect(page.locator('.ant-page-header').getByTitle(pageTitle)).toBeVisible(); }); test('edit page title', async ({ page, mockPage }) => { @@ -47,10 +47,10 @@ test.describe('page title', () => { await expect(page.getByTitle('page title1')).toBeVisible(); await page.getByTitle('page title1').click(); await page.getByLabel('designer-schema-settings-Page').hover(); - await page.getByText('Edit page title').click(); + await page.getByRole('menuitem', { name: 'Edit page title' }).click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('page title2'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByText('page title2').click(); await expect(page.getByText('page title2')).toBeVisible(); //菜单栏不调整 @@ -64,9 +64,9 @@ test.describe('page tabs', () => { await page.getByTitle('page tab').click(); await page.getByLabel('designer-schema-settings-Page').hover(); //默认不启用 - await expect(page.getByLabel('Enable page tabs').getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Enable page tabs' }).getByRole('switch')).not.toBeChecked(); //启用标签 - await page.getByLabel('Enable page tabs').click(); + await page.getByRole('menuitem', { name: 'Enable page tabs' }).click(); await expect(page.getByRole('tab').locator('div').filter({ hasText: 'Unnamed' })).toBeVisible(); await expect(page.getByLabel('schema-initializer-Page-tabs')).toBeVisible(); await page.getByRole('tab').locator('div').filter({ hasText: 'Unnamed' }).click(); @@ -76,60 +76,49 @@ test.describe('page tabs', () => { await page.getByLabel('schema-initializer-Page-tabs').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('page tab 1'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByText('page tab 1').click(); await page.getByLabel('schema-initializer-Page-tabs').click(); await page.getByRole('textbox').click(); await page.getByRole('textbox').fill('page tab 2'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByText('page tab 2').click(); - await page.waitForTimeout(1000); // 等待1秒钟 - const tabMenuItem = await page.getByRole('tab').locator('div > span').filter({ hasText: 'page tab 2' }); - const tabMenuItemActivedColor = await tabMenuItem.evaluate((element) => { - const computedStyle = window.getComputedStyle(element); - return computedStyle.color; - }); //激活的tab样式符合预期 await expect(page.getByText('page tab 1')).toBeVisible(); await expect(page.getByText('page tab 2')).toBeVisible(); await expect(page.getByLabel('schema-initializer-Grid-BlockInitializers')).toBeVisible(); - await expect(tabMenuItemActivedColor).toBe('rgb(22, 119, 255)'); + await expect(page.getByRole('tab', { name: 'page tab 2' })).toHaveAttribute('aria-selected', 'true'); //修改tab名称 - await page.getByText('Unnamed').click(); - await page.getByRole('button', { name: 'designer-schema-settings-Page-tab' }).click(); - await page.getByLabel('Edit', { exact: true }).click(); + await page.getByText('Unnamed').hover(); + await page.getByRole('tab', { name: 'Unnamed' }).getByLabel('designer-schema-settings-Page').hover(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); await page.getByRole('textbox').fill('page tab'); - await page.getByRole('button', { name: 'OK' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); const tabMenuItem1 = await page.getByRole('tab').getByText('page tab', { exact: true }); - const tabMenuItemActivedColor1 = await tabMenuItem1.evaluate((element) => { - const computedStyle = window.getComputedStyle(element); - return computedStyle.color; - }); await expect(tabMenuItem1).toBeVisible(); await expect(page.getByLabel('schema-initializer-Grid-BlockInitializers')).toBeVisible(); - await expect(tabMenuItemActivedColor1).toBe('rgb(22, 119, 255)'); //删除 tab await page.getByRole('tab').getByText('page tab', { exact: true }).hover(); - await page.getByRole('button', { name: 'designer-schema-settings-Page-tab' }).click(); - await page.getByRole('button', { name: 'Delete' }).click(); - await page.getByRole('button', { name: 'OK' }).click(); + await page + .getByRole('tab', { name: 'page tab designer-drag-handler' }) + .getByLabel('designer-schema-settings') + .hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); await expect(page.getByRole('tab').getByText('page tab', { exact: true })).not.toBeVisible(); await page.getByRole('tab').getByText('page tab 1').click(); //禁用标签 - await page - .locator('div') - .filter({ hasText: /^page tab$/ }) - .nth(1) - .click(); - await page.getByLabel('designer-schema-settings-Page', { exact: true }).click(); - await page.getByLabel('Enable page tabs').getByRole('switch').setChecked(false); + await page.getByTitle('page tab', { exact: true }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Page' }).hover(); + await page.getByRole('menuitem', { name: 'Enable page tabs' }).click(); await expect(page.getByText('page tab 2')).not.toBeVisible(); }); + test('drag page tab sorting', async ({ page, mockPage }) => { await mockPage({ pageSchema: { @@ -170,11 +159,11 @@ test.describe('page tabs', () => { }, }).goto(); - const sourceElement = await page.locator('span:has-text("tab 2")'); + const sourceElement = page.locator('span:has-text("tab 2")'); await sourceElement.hover(); - const source = await page.getByRole('button', { name: 'drag' }); + const source = page.getByRole('button', { name: 'drag' }); await source.hover(); - const targetElement = await page.locator('span:has-text("tab 1")'); + const targetElement = page.locator('span:has-text("tab 1")'); const sourceBoundingBox = await sourceElement.boundingBox(); const targetBoundingBox = await targetElement.boundingBox(); //拖拽标签调整排序 拖拽前 1-2 @@ -184,6 +173,6 @@ test.describe('page tabs', () => { const tab2 = await page.locator('span:has-text("tab 2")').boundingBox(); const tab1 = await page.locator('span:has-text("tab 1")').boundingBox(); //拖拽后 2-1 - await expect(tab2.x).toBeLessThan(tab1.x); + expect(tab2.x).toBeLessThan(tab1.x); }); }); diff --git a/packages/core/client/src/__tests__/e2e/pm.test.ts b/packages/core/client/src/__tests__/e2e/pm.test.ts index 463492bd94..e80d3af34f 100644 --- a/packages/core/client/src/__tests__/e2e/pm.test.ts +++ b/packages/core/client/src/__tests__/e2e/pm.test.ts @@ -1,7 +1,6 @@ import { expect, test } from '@nocobase/test/client'; async function waitForModalToBeHidden(page) { - test.slow(); await page.waitForFunction(() => { const modal = document.querySelector('.ant-modal'); if (modal) { @@ -13,6 +12,7 @@ async function waitForModalToBeHidden(page) { } test.describe('add plugin in front', () => { + test.slow(); test('add plugin npm registry,then remove plugin', async ({ page, mockPage }) => { await mockPage().goto(); await page.getByTestId('plugin-manager-button').click(); @@ -23,7 +23,6 @@ test.describe('add plugin in front', () => { .getByRole('textbox') .fill('@nocobase/plugin-sample-custom-collection-template'); await page.getByLabel('Submit').click(); - await page.waitForTimeout(1000); // 等待1秒钟 //等待页面刷新结束 await page.waitForFunction(() => { const modal = document.querySelector('.ant-modal'); @@ -39,7 +38,7 @@ test.describe('add plugin in front', () => { //将添加的插件删除 await page.getByLabel('sample-custom-collection-template').getByText('Remove').click(); await page.getByRole('button', { name: 'Yes' }).click(); - await page.waitForTimeout(2000); // 等待2秒钟 + await page.waitForTimeout(300); //等待页面刷新结束 await waitForModalToBeHidden(page); await page.waitForLoadState('load'); @@ -51,6 +50,7 @@ test.describe('add plugin in front', () => { }); test.describe('remove plugin', () => { + test.slow(); test('remove plugin,then add plugin', async ({ page, mockPage }) => { await mockPage().goto(); await page.getByTestId('plugin-manager-button').click(); @@ -74,7 +74,6 @@ test.describe('remove plugin', () => { .getByRole('textbox') .fill('@nocobase/plugin-sample-hello'); await page.getByLabel('Submit').click(); - await page.waitForTimeout(1000); //等待弹窗消失和页面刷新结束 await page.waitForFunction(() => { const modal = document.querySelector('.ant-modal'); @@ -97,27 +96,26 @@ test.describe('remove plugin', () => { }); test.describe('enable & disabled plugin', () => { + test.slow(); test('enable plugin', async ({ page, mockPage }) => { await mockPage().goto(); await page.getByTestId('plugin-manager-button').click(); await page.getByPlaceholder('Search plugin').fill('hello'); await expect(page.getByLabel('Hello')).toBeVisible(); - const isActive = await page.getByLabel('Hello').getByLabel('enable').isChecked(); - expect(isActive).toBe(false); - // 激活插件 + await expect(page.getByLabel('Hello').getByLabel('enable')).not.toBeChecked(); + //激活插件 await page.getByLabel('Hello').getByLabel('enable').click(); await page.waitForTimeout(1000); // 等待1秒钟 //等待弹窗消失和页面刷新结束 await waitForModalToBeHidden(page); await page.waitForLoadState('load'); await page.getByPlaceholder('Search plugin').fill('hello'); - await expect(await page.getByLabel('Hello').getByLabel('enable').isChecked()).toBe(true); + await expect(page.getByLabel('Hello').getByLabel('enable')).toBeChecked(); //将激活的插件禁用 await page.getByLabel('Hello').getByLabel('enable').click(); - await page.waitForTimeout(1000); // 等待1秒钟 //等待弹窗消失和页面刷新结束 await waitForModalToBeHidden(page); await page.waitForLoadState('load'); - await expect(await page.getByLabel('Hello').getByLabel('enable').isChecked()).toBe(false); + await expect(page.getByLabel('Hello').getByLabel('enable')).not.toBeChecked(); }); }); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/actions.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/actions.test.ts new file mode 100644 index 0000000000..28ede78202 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/actions.test.ts @@ -0,0 +1,141 @@ +import { + expect, + oneEmptyTableBlockWithActions, + oneEmptyTableBlockWithCustomizeActions, + test, +} from '@nocobase/test/client'; + +test.describe('Actions with dialog', () => { + test('add new', async ({ page, mockPage }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + // open dialog + await page.getByLabel('action-Action-Add new-create-general-table').click(); + + // add a tab + await page.getByLabel('schema-initializer-Tabs-TabPaneInitializersForCreateFormBlock-general').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test123'); + await page.getByLabel('action-Action-Submit-general-table').click(); + + await expect(page.getByText('test123')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-CreateFormBlockInitializers-general').hover(); + await page.getByText('Form').click(); + await page.getByText('Markdown').click(); + + await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); + }); + + test('add record', async ({ page, mockPage }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + // open dialog + await page.getByLabel('action-Action-Add record-customize:create-general-table').click(); + + // add a tab + await page.getByLabel('schema-initializer-Tabs-TabPaneInitializersForCreateFormBlock-general').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test7'); + await page.getByLabel('action-Action-Submit-general-table').click(); + + await expect(page.getByText('test7')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-CusomeizeCreateFormBlockInitializers-general').hover(); + await page.getByText('Form').hover(); + await page.getByRole('menuitem', { name: 'Users' }).click(); + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + + await expect(page.getByLabel('block-item-CardItem-users-form')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); + }); + + test('view & edit & popup', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + // open dialog + await page.getByLabel('action-Action.Link-View-view-general-table-0').click(); + + // add a tab + await page.getByLabel('schema-initializer-Tabs-TabPaneInitializers-general').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test8'); + await page.getByLabel('action-Action-Submit-general-table-0').click(); + + await expect(page.getByText('test8')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Details' }).click(); + await page.getByText('Form').click(); + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + + await expect(page.getByText('GeneralConfigure actionsConfigure fields')).toBeVisible(); + await expect(page.getByText('GeneralConfigure fieldsConfigure actions')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); + + // 删除已创建的 blocks,腾出页面空间 + // delete details block + await page.getByText('GeneralConfigure actionsConfigure fields').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // delete form block + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // delete markdown block + await page.getByLabel('block-item-Markdown.Void-general-markdown').hover(); + await page.getByLabel('designer-schema-settings-Markdown.Void-Markdown.Void.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // add relationship blocks + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Many to one' }).hover(); + await page.getByRole('menuitem', { name: 'Details' }).click(); + + await page.mouse.move(300, 0); + + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'One to many' }).hover(); + + // 下拉列表中,可选择以下区块进行创建 + await expect(page.getByText('Table')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Details' }).nth(1)).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'List' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Grid Card' })).toBeVisible(); + await expect(page.getByText('Form').nth(1)).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible(); + + await page.getByText('Table').click(); + await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); + }); + + test('bulk edit', async ({ page, mockPage }) => { + await mockPage(oneEmptyTableBlockWithCustomizeActions).goto(); + + // open dialog + await page.getByLabel('action-Action-Bulk edit-customize:bulkEdit-general-table').click(); + await page.getByLabel('schema-initializer-Tabs-TabPaneInitializersForBulkEditFormBlock-general').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test1'); + await page.getByLabel('action-Action-Submit-general-table').click(); + + await expect(page.getByText('test1')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-CreateFormBulkEditBlockInitializers-general').hover(); + await page.getByText('Form').click(); + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + + await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/associationFields.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/associationFields.test.ts new file mode 100644 index 0000000000..1691e5d063 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/associationFields.test.ts @@ -0,0 +1,103 @@ +import { + expect, + oneDetailBlockWithM2oFieldToGeneral, + oneFormBlockWithRolesFieldBasedUsers, + test, +} from '@nocobase/test/client'; + +test.describe('association fields with dialog', () => { + test('data selector', async ({ page, mockPage }) => { + await mockPage(oneFormBlockWithRolesFieldBasedUsers).goto(); + + // open dialog + await page.getByTestId('select-data-picker').click(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-TableSelectorInitializers-roles').hover(); + await page.getByRole('menuitem', { name: 'form Table' }).click(); + + // 筛选区块这里应该是可以直接点击的,不应该有二级菜单 + await page.getByText('Form').click(); + await page.getByRole('menuitem', { name: 'Collapse' }).click(); + + await page.getByRole('menuitem', { name: 'Add text' }).click(); + + await expect(page.getByLabel('block-item-CardItem-roles-table-selector')).toBeVisible(); + await expect(page.getByLabel('block-item-CardItem-roles-filter-form')).toBeVisible(); + + // 向下滚动一点距离,使得下方的区块可见 + await page.getByLabel('block-item-CardItem-roles-table-selector').hover(); + await page.mouse.wheel(0, 500); + + await expect(page.getByLabel('block-item-CardItem-roles-filter-collapse')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-roles-form')).toBeVisible(); + }); + + test('click association link to view', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneDetailBlockWithM2oFieldToGeneral).waitForInit(); + const record = await mockRecord('targetToGeneral'); + await nocoPage.goto(); + + // open dialog + await page + .getByLabel('block-item-CollectionField-targetToGeneral-details-targetToGeneral.toGeneral-toGeneral') + .getByText(record.id, { exact: true }) + .click(); + + // add a tab + await page.getByLabel('schema-initializer-Tabs-TabPaneInitializers-general').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('test8'); + await page.getByLabel('action-Action-Submit-general-details').click(); + + await expect(page.getByText('test8')).toBeVisible(); + + // add blocks + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Details' }).click(); + await page.getByText('Form').click(); + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + + await expect(page.getByText('GeneralConfigure actionsConfigure fields')).toBeVisible(); + await expect(page.getByText('GeneralConfigure fieldsConfigure actions')).toBeVisible(); + await expect(page.getByLabel('block-item-Markdown.Void-general-markdown')).toBeVisible(); + + // 删除已创建的 blocks,腾出页面空间 + // delete details block + await page.getByText('GeneralConfigure actionsConfigure fields').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // delete form block + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + // delete markdown block + await page.getByLabel('block-item-Markdown.Void-general-markdown').hover(); + await page.getByLabel('designer-schema-settings-Markdown.Void-Markdown.Void.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // add relationship blocks + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Many to one' }).hover(); + await page.getByRole('menuitem', { name: 'Details' }).click(); + + await page.mouse.move(300, 0); + + await page.getByLabel('schema-initializer-Grid-RecordBlockInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'One to many' }).hover(); + + // 下拉列表中,可选择以下区块进行创建 + await expect(page.getByText('Table')).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Details' }).nth(1)).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'List' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Grid Card' })).toBeVisible(); + await expect(page.getByText('Form').nth(1)).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible(); + + await page.getByText('Table').click(); + await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/detailsBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/detailsBlock.test.ts new file mode 100644 index 0000000000..7d45266ebe --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/detailsBlock.test.ts @@ -0,0 +1,82 @@ +import { expect, oneEmptyDetailsBlock, test } from '@nocobase/test/client'; + +test.describe('Details block', () => { + test('add fields', async ({ page, mockPage }) => { + await mockPage(oneEmptyDetailsBlock).goto(); + + const formItemInitializer = page.getByLabel('schema-initializer-Grid-ReadPrettyFormItemInitializers-general'); + + // add fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + + // add association fields + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-details-general.id-ID')).toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-details-general.manyToOne.nickname'), + ).toBeVisible(); + + // delete fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-details-general.id-ID')).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-details-general.manyToOne.nickname'), + ).not.toBeVisible(); + + // add text + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'Add text' }).click(); + await expect(page.getByLabel('block-item-Markdown.Void-general-details')).toBeVisible(); + }); + + test('add action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyDetailsBlock).goto(); + + await page.getByLabel('schema-initializer-ActionBar-DetailsActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Edit' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Duplicate' })).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-DetailsActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Edit' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Duplicate' })).not.toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/filterCollapseBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/filterCollapseBlock.test.ts new file mode 100644 index 0000000000..dc008d2625 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/filterCollapseBlock.test.ts @@ -0,0 +1,35 @@ +import { expect, oneEmptyFilterCollapseBlock, test } from '@nocobase/test/client'; + +test.describe('Filter Collapse', () => { + test('add fields', async ({ page, mockPage }) => { + await mockPage(oneEmptyFilterCollapseBlock).goto(); + + // add fields + await page + .getByLabel('schema-initializer-AssociationFilter-AssociationFilter.FilterBlockInitializer-general') + .hover(); + await page.getByRole('menuitem', { name: 'Created by' }).click(); + await page.getByRole('menuitem', { name: 'Single select' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Created by' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Single select' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Created by' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Single select' })).toBeVisible(); + + // delete fields + await page + .getByLabel('schema-initializer-AssociationFilter-AssociationFilter.FilterBlockInitializer-general') + .hover(); + await page.getByRole('menuitem', { name: 'Created by' }).click(); + await page.getByRole('menuitem', { name: 'Single select' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Created by' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Single select' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Created by' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Single select' })).not.toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/filterFormBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/filterFormBlock.test.ts new file mode 100644 index 0000000000..704e43b5ed --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/filterFormBlock.test.ts @@ -0,0 +1,86 @@ +import { expect, oneEmptyFilterFormBlock, test } from '@nocobase/test/client'; + +test.describe('Filter Form Block', () => { + test('add fields', async ({ page, mockPage }) => { + await mockPage(oneEmptyFilterFormBlock).goto(); + + const formItemInitializer = page.getByLabel('schema-initializer-Grid-FilterFormItemInitializers-general'); + + // add fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-filter-form-general.id-ID')).toBeVisible(); + + // delete fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-filter-form-general.id-ID')).not.toBeVisible(); + + // add association fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect( + page.getByLabel('block-item-CollectionField-general-filter-form-general.manyToOne.nickname'), + ).toBeVisible(); + + // delete association fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect( + page.getByLabel('block-item-CollectionField-general-filter-form-general.manyToOne.nickname'), + ).not.toBeVisible(); + + // add text + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'Add text' }).click(); + + await expect(page.getByLabel('block-item-Markdown.Void-general-filter-form')).toBeVisible(); + }); + + test.pgOnly('add inherit fields', async ({ page, mockPage }) => {}); + + test('add action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyFilterFormBlock).goto(); + + await page.getByLabel('schema-initializer-ActionBar-FilterFormActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Reset' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Reset' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action-Filter-submit-general-filter-form')).toBeVisible(); + await expect(page.getByLabel('action-Action-Reset-general-filter-form')).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-FilterFormActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Reset' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Reset' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action-Filter-submit-general-filter-form')).not.toBeVisible(); + await expect(page.getByLabel('action-Action-Reset-general-filter-form')).not.toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/formBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/formBlock.test.ts new file mode 100644 index 0000000000..fceccbf375 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/formBlock.test.ts @@ -0,0 +1,77 @@ +import { expect, oneEmptyForm, test } from '@nocobase/test/client'; + +test.describe('Form', () => { + test('add fields', async ({ page, mockPage }) => { + await mockPage(oneEmptyForm).goto(); + + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + + // add association fields + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.id-ID')).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne.nickname')).toBeVisible(); + + // delete fields + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.id-ID')).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.manyToOne.nickname'), + ).not.toBeVisible(); + + // add text + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Text' }).click(); + await expect(page.getByLabel('block-item-Markdown.Void-general-form')).toBeVisible(); + }); + + test.pgOnly('add inherit fields', async ({ page, mockPage }) => {}); + + test('add action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyForm).goto(); + + await page.getByLabel('schema-initializer-ActionBar-FormActionInitializers-general').hover(); + + // add button + await page.getByRole('menuitem', { name: 'Submit' }).click(); + await expect(page.getByRole('menuitem', { name: 'Submit' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + + // delete button + await page.getByLabel('schema-initializer-ActionBar-FormActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Submit' }).click(); + await expect(page.getByRole('menuitem', { name: 'Submit' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Submit' })).not.toBeVisible(); + }); + + test('add custom action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyForm).goto(); + + await page.getByLabel('schema-initializer-ActionBar-FormActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Save record' }).click(); + + await expect(page.getByRole('button', { name: 'Save record' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/gridCardBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/gridCardBlock.test.ts new file mode 100644 index 0000000000..ceb3f98e20 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/gridCardBlock.test.ts @@ -0,0 +1,146 @@ +import { expect, oneEmptyGridCardBlock, test } from '@nocobase/test/client'; + +test.describe('Grid Card', () => { + test('add actions', async ({ page, mockPage }) => { + await mockPage(oneEmptyGridCardBlock).goto(); + + await page.getByLabel('schema-initializer-ActionBar-GridCardActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-GridCardActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).not.toBeVisible(); + }); + + test('add item fields', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyGridCardBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + const formItemInitializer = page + .getByLabel('schema-initializer-Grid-ReadPrettyFormItemInitializers-general') + .first(); + + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + + // add association fields + await page.mouse.wheel(0, 300); + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-grid-card-general.id-ID').first()).toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-grid-card-general.manyToOne.nickname').first(), + ).toBeVisible(); + + // delete fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.wheel(0, 300); + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect( + page.getByLabel('block-item-CollectionField-general-grid-card-general.id-ID').first(), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-grid-card-general.manyToOne.nickname').first(), + ).not.toBeVisible(); + + // add text + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).hover(); + await page.mouse.wheel(0, 300); + await page.getByRole('menuitem', { name: 'Add text' }).click(); + + await expect(page.getByLabel('block-item-Markdown.Void-general-grid-card').first()).toBeVisible(); + }); + + test.pgOnly('add inherit fields', async ({ page, mockPage }) => {}); + + test('add item actions', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyGridCardBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByLabel('schema-initializer-ActionBar-GridCardItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-general-grid-card').first()).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-general-grid-card').first()).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-general-grid-card').first()).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-GridCardItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-general-grid-card').first()).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-general-grid-card').first()).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-general-grid-card').first()).not.toBeVisible(); + }); + + test('add item custom action buttons', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyGridCardBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByLabel('schema-initializer-ActionBar-GridCardItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Popup' }).click(); + await page.getByRole('menuitem', { name: 'Update record' }).click(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-Popup-customize:popup-general-grid-card').first()).toBeVisible(); + await expect( + page.getByLabel('action-Action.Link-Update record-customize:update-general-grid-card').first(), + ).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/listBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/listBlock.test.ts new file mode 100644 index 0000000000..6faef79802 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/listBlock.test.ts @@ -0,0 +1,141 @@ +import { expect, oneEmptyListBlock, test } from '@nocobase/test/client'; + +test.describe('List', () => { + test('add list actions', async ({ page, mockPage }) => { + await mockPage(oneEmptyListBlock).goto(); + + await page.getByLabel('schema-initializer-ActionBar-ListActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-ListActionInitializers-general').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).not.toBeVisible(); + }); + + test('add fields', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyListBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + const formItemInitializer = page + .getByLabel('schema-initializer-Grid-ReadPrettyFormItemInitializers-general') + .first(); + + // add fields + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + + // add association fields + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + // TODO: 二级菜单点击后,不应该被关闭;只有在鼠标移出下拉列表的时候,才应该被关闭 + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-list-general.id-ID').first()).toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-list-general.manyToOne.nickname').first(), + ).toBeVisible(); + + // delete fields + await formItemInitializer.hover(); + await page.getByRole('tooltip').getByText('Display association fields').hover(); + await page.mouse.wheel(0, -300); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + + await page.getByRole('menuitem', { name: 'Many to one' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('block-item-CollectionField-general-list-general.id-ID').first()).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-list-general.manyToOne.nickname').first(), + ).not.toBeVisible(); + + // add text + await formItemInitializer.hover(); + await page.getByRole('menuitem', { name: 'Add text' }).click(); + await expect(page.getByLabel('block-item-Markdown.Void-general-list').first()).toBeVisible(); + }); + + test('add item actions', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyListBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByLabel('schema-initializer-ActionBar-ListItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-general-list').first()).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-general-list').first()).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-general-list').first()).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-ListItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-general-list').first()).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-general-list').first()).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-general-list').first()).not.toBeVisible(); + }); + + test('add custom action buttons', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyListBlock).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByLabel('schema-initializer-ActionBar-ListItemActionInitializers-general').first().hover(); + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Popup' }).click(); + await page.getByRole('menuitem', { name: 'Update record' }).click(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-Popup-customize:popup-general-list').first()).toBeVisible(); + await expect( + page.getByLabel('action-Action.Link-Update record-customize:update-general-list').first(), + ).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/menu.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/menu.test.ts new file mode 100644 index 0000000000..713b0c7881 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/menu.test.ts @@ -0,0 +1,68 @@ +import { expect, groupPageEmpty, test } from '@nocobase/test/client'; + +test.describe('Menu', () => { + test('header', async ({ page, deletePage }) => { + await page.goto('/'); + + // add group + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Group' }).click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('page group'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('page group', { exact: true })).toBeVisible(); + + // add page + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('page item'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByLabel('page item', { exact: true })).toBeVisible(); + + // add link + await page.getByTestId('schema-initializer-Menu-header').hover(); + await page.getByRole('menuitem', { name: 'Link', exact: true }).click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill('page link'); + await page.getByLabel('block-item-Input-Link').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-Link').getByRole('textbox').fill('baidu.com'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByText('page link', { exact: true })).toBeVisible(); + + // delete pages + await deletePage('page group'); + await deletePage('page item'); + await deletePage('page link'); + }); + + test('sidebar', async ({ page, mockPage }) => { + await mockPage(groupPageEmpty).goto(); + + // add group in side + await page.getByTestId('schema-initializer-Menu-side').hover(); + await page.getByRole('menuitem', { name: 'Group', exact: true }).click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('page group side'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByText('page group side', { exact: true })).toBeVisible(); + + // add page in side + await page.getByTestId('schema-initializer-Menu-side').hover(); + await page.getByRole('menuitem', { name: 'Page', exact: true }).click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('page item side'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByText('page item side', { exact: true })).toBeVisible(); + + // add link in side + await page.getByTestId('schema-initializer-Menu-side').hover(); + await page.getByRole('menuitem', { name: 'Link', exact: true }).click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-Menu item title').getByRole('textbox').fill('link item side'); + await page.getByLabel('block-item-Input-Link').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-Link').getByRole('textbox').fill('/'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByText('link item side', { exact: true })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/pageGrid.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/pageGrid.test.ts new file mode 100644 index 0000000000..2404a425ad --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/pageGrid.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from '@nocobase/test/client'; +import { createBlock } from './utils'; + +test.describe('Page.Grid', () => { + test('create block', async ({ page, mockPage }) => { + await mockPage().goto(); + const button = page.getByLabel('schema-initializer-Grid-BlockInitializers'); + + // add table block + await button.hover(); + await createBlock(page, 'Table'); + await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); + + // add form block + await button.hover(); + await createBlock(page, 'Form'); + await expect(page.getByLabel('block-item-CardItem-users-form')).toBeVisible(); + + // add detail block + await button.hover(); + await createBlock(page, 'Details'); + await expect(page.getByLabel('block-item-CardItem-users-details')).toBeVisible(); + + // add list block + await button.hover(); + await createBlock(page, 'List'); + await expect(page.getByLabel('block-item-CardItem-users-list')).toBeVisible(); + + // add grid card block + await button.hover(); + await createBlock(page, 'Grid Card'); + await expect(page.getByLabel('block-item-BlockItem-users-grid-card')).toBeVisible(); + + // add filter form block + await button.hover(); + await createBlock(page, 'Filter form'); + await expect(page.getByLabel('block-item-CardItem-users-filter-form')).toBeVisible(); + + // add collapse block + await button.hover(); + await createBlock(page, 'Collapse'); + await expect(page.getByLabel('block-item-CardItem-users-filter-collapse')).toBeVisible(); + + // add markdown block + await button.hover(); + await createBlock(page, 'Markdown'); + await expect(page.getByLabel('block-item-Markdown.Void-markdown')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/pageTabs.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/pageTabs.test.ts new file mode 100644 index 0000000000..5e214c3dee --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/pageTabs.test.ts @@ -0,0 +1,15 @@ +import { expect, tabPageEmpty, test } from '@nocobase/test/client'; + +test.describe('Page.Tabs', () => { + test('enable to create tab', async ({ page, mockPage }) => { + await mockPage(tabPageEmpty).goto(); + + // add tab page + await page.getByLabel('schema-initializer-Page-tabs').click(); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('tab1'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByText('tab1', { exact: true })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/tableBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/tableBlock.test.ts new file mode 100644 index 0000000000..58b8a5806a --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/tableBlock.test.ts @@ -0,0 +1,220 @@ +import { expect, oneEmptyTable, test } from '@nocobase/test/client'; + +test.describe('Table', () => { + test('add column', async ({ page, mockPage }) => { + await mockPage(oneEmptyTable).goto(); + const configureColumnButton = page.getByLabel('schema-initializer-TableV2-TableColumnInitializers-t_unp4scqamw9'); + + // Action column ------------------------------------------------------------- + // 1. 点击开关,可以开启和关闭 Action column + // 2. 点击开关之后,如果不移出鼠标,下拉框不应该关闭 + await expect(page.getByText('Actions', { exact: true })).toBeVisible(); + await configureColumnButton.hover(); + + // 滚动鼠标,以把底部的 Action column 选项显示出来 + await page.getByText('Display collection fields', { exact: true }).hover(); + await page.mouse.wheel(0, 300); + + await expect(page.getByRole('menuitem', { name: 'Action column' }).getByRole('switch')).toBeChecked(); + await page.getByRole('menuitem', { name: 'Action column' }).click(); + await expect(page.getByRole('menuitem', { name: 'Action column' }).getByRole('switch')).not.toBeChecked(); + + // 移动鼠标关闭下拉框 + await page.mouse.move(300, 0); + await expect(page.getByText('Actions', { exact: true })).not.toBeVisible(); + + // 再次开启 Action column + await configureColumnButton.hover(); + await page.mouse.wheel(0, 300); + await page.getByRole('menuitem', { name: 'Action column' }).click(); + await expect(page.getByRole('menuitem', { name: 'Action column' }).getByRole('switch')).toBeChecked(); + await page.mouse.move(300, 0); + await expect(page.getByText('Actions', { exact: true })).toBeVisible(); + + // collection fields ------------------------------------------------------------- + await configureColumnButton.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).first().click(); + await page.getByRole('menuitem', { name: 'One to one (has one)' }).first().click(); + await page.getByRole('menuitem', { name: 'Many to one' }).first().click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'One to one (belongs to)' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'One to one (has one)' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Many to one' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('button', { name: 'ID', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'One to one (belongs to)', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'One to one (has one)', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Many to one', exact: true })).toBeVisible(); + + // 点击开关,删除创建的字段 + await configureColumnButton.hover(); + await page.getByRole('menuitem', { name: 'ID' }).click(); + await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).first().click(); + await page.getByRole('menuitem', { name: 'One to one (has one)' }).first().click(); + await page.getByRole('menuitem', { name: 'Many to one' }).first().click(); + await expect(page.getByRole('menuitem', { name: 'ID' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'One to one (belongs to)' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'One to one (has one)' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Many to one' }).getByRole('switch')).not.toBeChecked(); + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'ID', exact: true })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'One to one (belongs to)', exact: true })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'One to one (has one)', exact: true })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Many to one', exact: true })).not.toBeVisible(); + + // association fields ------------------------------------------------------------- + await configureColumnButton.hover(); + + await page.getByText('Display collection fields', { exact: true }).hover(); + await page.mouse.wheel(0, 300); + + await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).nth(1).hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await expect(page.getByRole('button', { name: 'Nickname', exact: true })).toBeVisible(); + + // 开关应该是开启状态 + await configureColumnButton.hover(); + await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).toBeChecked(); + + // 点击开关,删除创建的字段 + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await expect(page.getByRole('button', { name: 'Nickname', exact: true })).not.toBeVisible(); + // 开关应该是关闭状态 + await configureColumnButton.hover(); + await page.getByRole('menuitem', { name: 'One to one (belongs to)' }).nth(1).hover(); + await expect(page.getByRole('menuitem', { name: 'Nickname' }).getByRole('switch')).not.toBeChecked(); + }); + + // TODO: 继承表相关测试 + test.pgOnly('inherit column', async ({ page, mockPage }) => {}); + + test('add column action buttons', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTable).waitForInit(); + await mockRecord('t_unp4scqamw9'); + await nocoPage.goto(); + + // add view & Edit & Delete & Duplicate ------------------------------------------------------------ + await page.getByText('Actions', { exact: true }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-t_unp4scqamw9-table-0')).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-t_unp4scqamw9-table-0')).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-t_unp4scqamw9-table-0')).toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Duplicate-duplicate-t_unp4scqamw9-table-0')).toBeVisible(); + + // delete view & Edit & Delete & Duplicate ------------------------------------------------------------ + await page.getByText('Actions', { exact: true }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + await expect(page.getByRole('menuitem', { name: 'View' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Edit' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Duplicate' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-View-view-t_unp4scqamw9-table-0')).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Edit-update-t_unp4scqamw9-table-0')).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Delete-destroy-t_unp4scqamw9-table-0')).not.toBeVisible(); + await expect(page.getByLabel('action-Action.Link-Duplicate-duplicate-t_unp4scqamw9-table-0')).not.toBeVisible(); + + // add custom action ------------------------------------------------------------ + await page.getByText('Actions', { exact: true }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + + await page.getByRole('menuitem', { name: 'Popup' }).click(); + // 此时二级菜单,不应该关闭,可以继续点击? + await page.getByRole('menuitem', { name: 'Update record' }).click(); + + await page.mouse.move(300, 0); + await expect(page.getByLabel('action-Action.Link-Popup-customize:popup-t_unp4scqamw9-table-0')).toBeVisible(); + await expect( + page.getByLabel('action-Action.Link-Update record-customize:update-t_unp4scqamw9-table-0'), + ).toBeVisible(); + }); + + test('adjust column width', async ({ page, mockPage }) => { + await mockPage(oneEmptyTable).goto(); + + // 列宽度默认为 200 + await expect(page.getByRole('columnheader', { name: 'Actions', exact: true })).toHaveJSProperty('offsetWidth', 200); + + await page.getByText('Actions', { exact: true }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'Column width' }).click(); + + await expect(page.getByRole('dialog').getByText('Column width')).toBeVisible(); + + await page.getByRole('dialog').getByRole('spinbutton').click(); + await page.getByRole('dialog').getByRole('spinbutton').fill('400'); + await page.getByRole('button', { name: 'Submit' }).click(); + + await expect(page.getByRole('columnheader', { name: 'Actions', exact: true })).toHaveJSProperty('offsetWidth', 400); + }); + + test('add action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyTable).goto(); + + // add buttons + await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).toBeVisible(); + + // delete buttons + await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'Filter' }).click(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Refresh' }).click(); + + await expect(page.getByRole('menuitem', { name: 'Filter' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Add new' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Delete' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByRole('menuitem', { name: 'Refresh' }).getByRole('switch')).not.toBeChecked(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Filter' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Add new' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete' })).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Refresh' })).not.toBeVisible(); + }); + + test('add custom action buttons', async ({ page, mockPage }) => { + await mockPage(oneEmptyTable).goto(); + + await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-t_unp4scqamw9').hover(); + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Bulk update' }).click(); + + await page.mouse.move(300, 0); + await expect(page.getByRole('button', { name: 'Bulk update' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaInitializer/utils.ts b/packages/core/client/src/__tests__/e2e/schemaInitializer/utils.ts new file mode 100644 index 0000000000..a00bc4a585 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaInitializer/utils.ts @@ -0,0 +1,21 @@ +import { Page } from '@playwright/test'; + +export const createBlock = async (page: Page, name: string) => { + await page.getByLabel('schema-initializer-Grid-BlockInitializers').hover(); + + if (name === 'Form') { + await page.getByText('Form', { exact: true }).first().hover(); + } else if (name === 'Filter form') { + await page.getByText('Form', { exact: true }).nth(1).hover(); + } else { + await page.getByText(name, { exact: true }).hover(); + } + + if (name === 'Markdown') { + await page.getByRole('menuitem', { name: 'Markdown' }).click(); + } else { + await page.getByRole('menuitem', { name: 'Users' }).click(); + } + + await page.mouse.move(300, 0); +}; diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addChild.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addChild.test.ts new file mode 100644 index 0000000000..8d1f42150b --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addChild.test.ts @@ -0,0 +1,38 @@ +import { Page, oneEmptyTableWithTreeCollection, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('add child', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Add child-create-treeCollection-table-0').hover(); + await page.getByLabel('designer-schema-settings-Action.Link-Action.Designer-tree').hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableWithTreeCollection).waitForInit(); + await nocoPage.goto(); + + // 添加一行数据 + // TODO: 使用 mockRecord 为 tree 表添加一行数据无效 + await page.getByLabel('schema-initializer-ActionBar-TableActionInitializers-treeCollection').hover(); + await page.getByRole('menuitem', { name: 'Add new' }).click(); + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByLabel('schema-initializer-Grid-CreateFormBlockInitializers-treeCollection').hover(); + await page.getByRole('menuitem', { name: 'form Form' }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('schema-initializer-ActionBar-CreateFormActionInitializers-treeCollection').hover(); + await page.getByRole('menuitem', { name: 'Submit' }).click(); + await page.mouse.move(300, 0); + await page.getByRole('button', { name: 'Submit' }).click(); + + // 添加 add child 按钮 + await page.getByRole('button', { name: 'Actions' }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-tree').hover(); + await page.getByRole('menuitem', { name: 'Add child' }).click(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addNew.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addNew.test.ts new file mode 100644 index 0000000000..3133f88ccf --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addNew.test.ts @@ -0,0 +1,86 @@ +import { Page, expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('add new', () => { + const showMenu = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Open mode', 'Popup size', 'Delete'], + }); + }); + + test('edit button', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await showMenu(page); + await page.getByRole('menuitem', { name: 'Edit button' }).click(); + await page.getByLabel('block-item-Input-general-Button title').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-general-Button title').getByRole('textbox').fill('1234'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByRole('button', { name: '1234' })).toBeVisible(); + }); + + test('open mode', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + await showMenu(page); + + // 默认是 drawer + await expect(page.getByRole('menuitem', { name: 'Open mode' }).getByText('Drawer')).toBeVisible(); + + // 切换为 dialog + await page.getByRole('menuitem', { name: 'Open mode' }).click(); + await page.getByRole('option', { name: 'Dialog' }).click(); + + await page.getByRole('button', { name: 'Add new' }).click(); + await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible(); + }); + + test('popup size', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await showMenu(page); + // 默认值 middle + await expect(page.getByRole('menuitem', { name: 'Popup size' }).getByText('Middle')).toBeVisible(); + + // 切换为 small + await page.getByRole('menuitem', { name: 'Popup size' }).click(); + await page.getByRole('option', { name: 'Small' }).click(); + + await page.getByRole('button', { name: 'Add new' }).click(); + const drawerWidth = + (await page.getByTestId('drawer-Action.Container-general-Add record').boundingBox())?.width || 0; + expect(drawerWidth).toBeLessThanOrEqual(400); + + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + + // 切换为 large + await showMenu(page); + await page.getByRole('menuitem', { name: 'Popup size' }).click(); + await page.getByRole('option', { name: 'Large' }).click(); + + await page.getByRole('button', { name: 'Add new' }).click(); + const drawerWidth2 = + (await page.getByTestId('drawer-Action.Container-general-Add record').boundingBox())?.width || 0; + expect(drawerWidth2).toBeGreaterThanOrEqual(800); + }); + + test('delete', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await showMenu(page); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.mouse.move(300, 0); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByRole('button', { name: 'Add new' })).toBeHidden(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addRecord.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addRecord.test.ts new file mode 100644 index 0000000000..8cb459b940 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/addRecord.test.ts @@ -0,0 +1,19 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('add record', () => { + const showMenu = async (page: Page) => { + await page.getByRole('button', { name: 'Add record' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/bulkDelete.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/bulkDelete.test.ts new file mode 100644 index 0000000000..e95b0db2cc --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/bulkDelete.test.ts @@ -0,0 +1,17 @@ +import { oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('bulk delete', () => { + test('supported options', async ({ page, mockPage }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await expectOptions({ + page, + showMenu: async () => { + await page.getByRole('button', { name: 'Delete' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + }, + enableOptions: ['Edit button', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/delete.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/delete.test.ts new file mode 100644 index 0000000000..2c6d9f2cf2 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/delete.test.ts @@ -0,0 +1,35 @@ +import { Page, expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('delete', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Delete-destroy-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Delete'], + }); + }); + + test('edit button', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await showMenu(page); + await page.getByRole('menuitem', { name: 'Edit button' }).click(); + await page.getByLabel('block-item-Input-general-Button title').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-general-Button title').getByRole('textbox').fill('Delete record'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByLabel('action-Action.Link-Delete record-destroy-general-table-0')).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/duplicate.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/duplicate.test.ts new file mode 100644 index 0000000000..db384fd538 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/duplicate.test.ts @@ -0,0 +1,21 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('duplicate', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Duplicate-duplicate-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Duplicate mode', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/edit.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/edit.test.ts new file mode 100644 index 0000000000..3e52863096 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/edit.test.ts @@ -0,0 +1,21 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('edit', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit-update-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/expectOptions.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/expectOptions.ts new file mode 100644 index 0000000000..928d3a23b9 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/expectOptions.ts @@ -0,0 +1,8 @@ +import { expect } from '@nocobase/test/client'; + +export async function expectOptions({ showMenu, enableOptions, page }) { + await showMenu(); + for (const option of enableOptions) { + await expect(page.getByRole('menuitem', { name: option })).toBeVisible(); + } +} diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/filter.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/filter.test.ts new file mode 100644 index 0000000000..0f5979628f --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/filter.test.ts @@ -0,0 +1,30 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('filter', () => { + const showMenu = async (page: Page) => { + await page.getByRole('button', { name: 'Filter' }).hover(); + await page.getByLabel('designer-schema-settings-Filter.Action-Filter.Action.Designer-general').hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: [ + 'Edit button', + 'Delete', + 'Many to one', + 'One to many', + 'Single select', + 'ID', + 'Created at', + 'Last updated at', + 'Created by', + 'Last updated by', + ], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/popup.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/popup.test.ts new file mode 100644 index 0000000000..3ecbe50a24 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/popup.test.ts @@ -0,0 +1,38 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +const addSomeCustomActions = async (page: Page) => { + // 先删除掉之前的 actions + await page.getByRole('button', { name: 'Actions' }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-general').hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // 再增加两个自定义的 actions + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Popup' }).click(); + await page.getByRole('menuitem', { name: 'Update record' }).click(); +}; + +test.describe('popup', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Popup-customize:popup-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + await addSomeCustomActions(page); + + await showMenu(page); + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/refresh.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/refresh.test.ts new file mode 100644 index 0000000000..9f40a3b167 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/refresh.test.ts @@ -0,0 +1,17 @@ +import { oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('refresh', () => { + test('supported options', async ({ page, mockPage }) => { + await mockPage(oneEmptyTableBlockWithActions).goto(); + + await expectOptions({ + page, + showMenu: async () => { + await page.getByRole('button', { name: 'Refresh' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover(); + }, + enableOptions: ['Edit button', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/saveRecord.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/saveRecord.test.ts new file mode 100644 index 0000000000..09021e673e --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/saveRecord.test.ts @@ -0,0 +1,25 @@ +import { oneEmptyFormWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('save record', () => { + test('supported options', async ({ page, mockPage }) => { + await mockPage(oneEmptyFormWithActions).goto(); + + await expectOptions({ + page, + showMenu: async () => { + await page.getByRole('button', { name: 'Save record' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-users' }).hover(); + }, + enableOptions: [ + 'Edit button', + 'Linkage rules', + 'Assign field values', + 'Skip required validation', + 'After successful submission', + 'Bind workflows', + 'Delete', + ], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/submit.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/submit.test.ts new file mode 100644 index 0000000000..d468af5696 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/submit.test.ts @@ -0,0 +1,17 @@ +import { oneEmptyFormWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('submit', () => { + test('supported options', async ({ page, mockPage }) => { + await mockPage(oneEmptyFormWithActions).goto(); + + await expectOptions({ + page, + showMenu: async () => { + await page.getByRole('button', { name: 'Submit' }).hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-users' }).hover(); + }, + enableOptions: ['Edit button', 'Linkage rules', 'Bind workflows', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/updateRecord.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/updateRecord.test.ts new file mode 100644 index 0000000000..a40aecb389 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/updateRecord.test.ts @@ -0,0 +1,38 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +const addSomeCustomActions = async (page: Page) => { + // 先删除掉之前的 actions + await page.getByRole('button', { name: 'Actions' }).hover(); + await page.getByLabel('designer-schema-settings-TableV2.Column-TableV2.ActionColumnDesigner-general').hover(); + await page.getByRole('menuitem', { name: 'View' }).click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + + // 再增加两个自定义的 actions + await page.getByRole('menuitem', { name: 'Customize' }).hover(); + await page.getByRole('menuitem', { name: 'Popup' }).click(); + await page.getByRole('menuitem', { name: 'Update record' }).click(); +}; + +test.describe('update record', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-Update record-customize:update-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + await addSomeCustomActions(page); + + await showMenu(page); + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Assign field values', 'After successful submission', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/actions/view.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/view.test.ts new file mode 100644 index 0000000000..1125015ed2 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/actions/view.test.ts @@ -0,0 +1,21 @@ +import { Page, oneEmptyTableBlockWithActions, test } from '@nocobase/test/client'; +import { expectOptions } from './expectOptions'; + +test.describe('view', () => { + const showMenu = async (page: Page) => { + await page.getByLabel('action-Action.Link-View-view-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + }; + + test('supported options', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneEmptyTableBlockWithActions).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await expectOptions({ + page, + showMenu: () => showMenu(page), + enableOptions: ['Edit button', 'Linkage rules', 'Open mode', 'Popup size', 'Delete'], + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formBlock.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formBlock.test.ts new file mode 100644 index 0000000000..9918f14ac2 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formBlock.test.ts @@ -0,0 +1,286 @@ +import { Page, expect, oneTableBlockWithActionsAndFormBlocks, test } from '@nocobase/test/client'; + +const clickOption = async (page: Page, optionName: string) => { + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByRole('menuitem', { name: optionName }).click(); +}; + +test.describe('SchemaSettings: creating form block', () => { + test('Edit block title', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithActionsAndFormBlocks).goto(); + await page.getByRole('button', { name: 'Add new' }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Edit block title'); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('Block title 123'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + const runExpect = async () => { + // 设置成功后,显示在区块顶部 + await expect( + page.getByLabel('block-item-CardItem-general-form').getByText('Block title 123', { exact: true }), + ).toBeVisible(); + + // 再次打开编辑弹窗时,显示的是上次设置的值 + await clickOption(page, 'Edit block title'); + await expect(page.getByRole('textbox')).toHaveValue('Block title 123'); + }; + + await runExpect(); + + // 刷新页面后,显示的应该依然是上次设置的值 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await runExpect(); + }); + test('Linkage rules', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithActionsAndFormBlocks).goto(); + await page.getByRole('button', { name: 'Add new' }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Linkage rules'); + await page.getByRole('button', { name: 'Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByText('singleLineText', { exact: true }).click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Visible' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + const runExpect = async () => { + await expect(page.getByTestId('select-linkage-property-field').getByText('singleLineText')).toBeVisible(); + await expect(page.getByTestId('select-linkage-action-field').getByText('Visible')).toBeVisible(); + }; + + // 再次打开,设置的值应该存在 + await clickOption(page, 'Linkage rules'); + await runExpect(); + + // 刷新页面后,设置的值应该依然存在 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await clickOption(page, 'Linkage rules'); + await runExpect(); + }); + test('Form data templates', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithActionsAndFormBlocks).goto(); + await page.getByRole('button', { name: 'Add new' }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Form data templates'); + await page.getByRole('button', { name: 'Add template' }).click(); + + await page.getByLabel('block-item-Select-general-Title field').getByTestId('select-single').click(); + await page.getByRole('option', { name: 'singleLineText' }).click(); + await page.getByRole('button', { name: 'singleLineText (Duplicate)' }).click(); + + await expect( + page.getByRole('button', { name: 'singleLineText (Duplicate)' }).locator('.ant-tree-checkbox'), + ).toHaveClass(/ant-tree-checkbox-checked/); + + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 区块顶部应该显示 Data template 选项 + await expect(page.getByText('Data template:', { exact: true })).toBeVisible(); + + // 再次打开,设置的值应该存在 + await clickOption(page, 'Form data templates'); + await expect(page.getByLabel('block-item-Select-general-Title field').getByTestId('select-single')).toHaveText( + 'singleLineText', + ); + await expect( + page.getByRole('button', { name: 'singleLineText (Duplicate)' }).locator('.ant-tree-checkbox'), + ).toHaveClass(/ant-tree-checkbox-checked/); + + // 刷新页面后,设置的值应该还在 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await expect(page.getByText('Data template:', { exact: true })).toBeVisible(); + + await clickOption(page, 'Form data templates'); + await expect(page.getByLabel('block-item-Select-general-Title field').getByTestId('select-single')).toHaveText( + 'singleLineText', + ); + await expect( + page.getByRole('button', { name: 'singleLineText (Duplicate)' }).locator('.ant-tree-checkbox'), + ).toHaveClass(/ant-tree-checkbox-checked/); + }); + test('Convert reference to duplicate & Save as block template', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithActionsAndFormBlocks).goto(); + await page.getByRole('button', { name: 'Add new' }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Save as block template'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 设置成功后,不再显示 Save as block template 选项,而是显示 Convert reference to duplicate 选项 + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).toBeVisible(); + + // 刷新页面 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).toBeVisible(); + + // Convert reference to duplicate + await clickOption(page, 'Convert reference to duplicate'); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + + // 刷新页面 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + }); + test('Delete', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithActionsAndFormBlocks).goto(); + + await page.getByRole('button', { name: 'Add new' }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + + // 打开编辑弹窗 + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 设置成功后,显示在区块顶部 + await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); + + // 刷新页面后,区块依然是被删除状态 + await page.reload(); + await page.getByRole('button', { name: 'Add new' }).click(); + await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); + }); +}); + +test.describe('SchemaSettings: editing list block', () => { + test('Edit block title', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + await page.getByText('Edit', { exact: true }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Edit block title'); + await page.getByRole('textbox').click(); + await page.getByRole('textbox').fill('Block title 123'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + const runExpect = async () => { + // 设置成功后,显示在区块顶部 + await expect( + page.getByLabel('block-item-CardItem-general-form').getByText('Block title 123', { exact: true }), + ).toBeVisible(); + + // 再次打开编辑弹窗时,显示的是上次设置的值 + await clickOption(page, 'Edit block title'); + await expect(page.getByRole('textbox')).toHaveValue('Block title 123'); + }; + + await runExpect(); + + // 刷新页面后,显示的应该依然是上次设置的值 + await page.reload(); + await page.getByText('Edit', { exact: true }).click(); + await runExpect(); + }); + test('Linkage rules', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + await page.getByText('Edit', { exact: true }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Linkage rules'); + await page.getByRole('button', { name: 'Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByText('singleLineText', { exact: true }).click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Visible' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + const runExpect = async () => { + await expect(page.getByTestId('select-linkage-property-field').getByText('singleLineText')).toBeVisible(); + await expect(page.getByTestId('select-linkage-action-field').getByText('Visible')).toBeVisible(); + }; + + // 再次打开,设置的值应该存在 + await clickOption(page, 'Linkage rules'); + await runExpect(); + + // 刷新页面后,设置的值应该依然存在 + await page.reload(); + await page.getByText('Edit', { exact: true }).click(); + await clickOption(page, 'Linkage rules'); + await runExpect(); + }); + test('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + await page.getByText('Edit', { exact: true }).click(); + + // 打开编辑弹窗 + await clickOption(page, 'Save as block template'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 设置成功后,不再显示 Save as block template 选项,而是显示 Convert reference to duplicate 选项 + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).toBeVisible(); + + // 刷新页面 + await page.reload(); + await page.getByText('Edit', { exact: true }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).toBeVisible(); + + // Convert reference to duplicate + await clickOption(page, 'Convert reference to duplicate'); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + + // 刷新页面 + await page.reload(); + await page.getByText('Edit', { exact: true }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Convert reference to duplicate' })).not.toBeVisible(); + }); + test('Delete', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByText('Edit', { exact: true }).click(); + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + + // 打开编辑弹窗 + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 设置成功后,显示在区块顶部 + await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); + + // 刷新页面后,区块依然是被删除状态 + await page.reload(); + await page.getByText('Edit', { exact: true }).click(); + await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formDataTemplates.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formDataTemplates.test.ts new file mode 100644 index 0000000000..fb4d472840 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formDataTemplates.test.ts @@ -0,0 +1,94 @@ +import { expect, oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test } from '@nocobase/test/client'; + +test.describe('formDataTemplates', () => { + test('basic usage', async ({ page, mockPage, mockRecords }) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).waitForInit(); + const records = await mockRecords('general', 3); + await nocoPage.goto(); + + const record = records.find((item) => item.id === 2); + + const openDialog = async () => { + await page.getByLabel('block-item-CardItem-general-form').hover(); + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Form data templates' }).click(); + }; + + await page.getByRole('button', { name: 'Add new' }).click(); + await openDialog(); + await page.getByRole('button', { name: 'plus Add template' }).click(); + + // 非继承表是不需要显示 Collection 选项的 + await expect(page.getByText('Collection:')).toBeHidden(); + + // 添加一个数据范围,条件是:ID = 2 + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').getByLabel('Search').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID' }).click(); + await page.getByLabel('Form data templates').getByRole('spinbutton').click(); + await page.getByLabel('Form data templates').getByRole('spinbutton').fill('2'); + + // 选择 ID 作为 title field + await page.getByTestId('select-single').click(); + await page.getByRole('option', { name: 'ID' }).click(); + + // 仅选中一个字段 + await page.getByRole('button', { name: 'singleLineText (Duplicate)' }).click(); + + // 保存 + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 保存成功后应该显示 Data template 选项 + await expect(page.getByText('Data template:')).toBeVisible(); + + // 选择一个模板 + await page.getByTestId('select-form-data-template').click(); + await page.getByRole('option', { name: 'Template name 1' }).click(); + await page.getByTestId('select-object-undefined').click(); + + // 因为添加了数据范围,所以只显示一个选项 + await expect(page.getByRole('option')).toHaveCount(1); + + // 选中数据 + await page.getByRole('option', { name: '2' }).click(); + + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue(record.singleLineText); + + // 其它未选中的字段的数据应该是空的 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toBeEmpty(); + + // 同步表单字段 + await openDialog(); + await page.getByLabel('action-Action.Link-Sync from form fields-general').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 重新选择一下数据,字段值才会被填充 + // TODO: 保存后,数据应该直接被填充上 + await page.getByLabel('icon-close-select').click(); + await page.getByTestId('select-object-undefined').getByLabel('Search').click(); + await page.getByRole('option', { name: '2' }).click(); + + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toHaveValue(record.email); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toHaveValue(String(record.integer)); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toHaveValue(String(record.number)); + + // 隐藏模板选项 + await openDialog(); + await page.getByLabel('Display data template selector').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByText('Data template:')).toBeHidden(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/commonTesting.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/commonTesting.ts new file mode 100644 index 0000000000..d1a3f3da28 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/commonTesting.ts @@ -0,0 +1,278 @@ +import { Page, expect, test } from '@nocobase/test/client'; + +export async function testEditFieldTitle(page: Page) { + await page.getByRole('menuitem', { name: 'Edit field title' }).click(); + await page.getByLabel('block-item-Input-general-').getByRole('textbox').click(); + await page.getByLabel('block-item-Input-general-').getByRole('textbox').fill('testing edit field title'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByText('testing edit field title')).toBeVisible(); +} + +export async function testDisplayTitle(page: Page, title: string) { + // 默认情况下是开启状态 + await expect(page.getByRole('menuitem', { name: 'Display title' }).getByRole('switch')).toBeChecked(); + await page.getByRole('menuitem', { name: 'Display title' }).click(); + await expect(page.getByRole('menuitem', { name: 'Display title' }).getByRole('switch')).not.toBeChecked(); + await expect(page.getByText(`${title}:`, { exact: true })).not.toBeVisible(); +} + +export async function testEditDescription(page: Page) { + await page.getByRole('menuitem', { name: 'Edit description' }).click(); + await page.getByLabel('block-item-Input.TextArea-').getByRole('textbox').click(); + await page.getByLabel('block-item-Input.TextArea-').getByRole('textbox').fill('testing edit description'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByText('testing edit description')).toBeVisible(); +} +export async function testRequired(page: Page) { + // 默认情况下是关闭状态 + await expect(page.getByRole('menuitem', { name: 'Required' }).getByRole('switch')).not.toBeChecked(); + await page.getByRole('menuitem', { name: 'Required' }).click(); + await expect(page.getByRole('menuitem', { name: 'Required' }).getByRole('switch')).toBeChecked(); +} + +export async function clickDeleteAndOk(page: Page) { + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); +} + +export const commonTesting = ({ + openDialogAndShowMenu, + fieldName, + blockType = 'creating', + fieldType, + mode = 'details', +}: { + openDialogAndShowMenu: ({ + page, + mockPage, + mockRecord, + mockRecords, + fieldName, + }: { + page: Page; + mockPage: any; + mockRecord: any; + mockRecords: any; + fieldName: string; + }) => Promise; + fieldName: string; + blockType?: 'creating' | 'viewing' | 'editing'; + fieldType?: 'relation' | 'system'; + /** + * options 模式下,只会测试选项是否正确显示; + * details 模式下,会测试每个选项的功能是否正常; + */ + mode?: 'options' | 'details'; +}) => { + if (mode === 'details') { + test('edit field title', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await testEditFieldTitle(page); + }); + + test('display title', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await testDisplayTitle(page, fieldName); + }); + + test('delete', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await clickDeleteAndOk(page); + await expect(page.getByText(`${fieldName}:`)).not.toBeVisible(); + }); + + if (['creating', 'editing'].includes(blockType)) { + test('edit description', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await testEditDescription(page); + }); + + test('required', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await testRequired(page); + }); + } + + if (blockType === 'viewing') { + test('edit tooltip', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await page.getByRole('menuitem', { name: 'Edit tooltip' }).click(); + await page.getByRole('dialog').getByRole('textbox').click(); + await page.getByRole('dialog').getByRole('textbox').fill('testing edit tooltip'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await page.getByRole('img', { name: 'question-circle' }).hover(); + await expect(page.getByText('testing edit tooltip')).toBeVisible(); + }); + } + } + + if (mode === 'options') { + test('should display correct options', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName }); + await expect(page.getByRole('menuitem', { name: 'Edit field title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Display title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + if (['creating', 'editing'].includes(blockType) && fieldType !== 'system') { + await expect(page.getByRole('menuitem', { name: 'Edit description' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Required' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).toBeVisible(); + } + if (blockType === 'viewing') { + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).not.toBeVisible(); + } + if ( + blockType === 'creating' && + !fieldName.startsWith('oneTo') && + !['attachment'].includes(fieldName) && + !['system'].includes(fieldType) + ) { + await expect(page.getByRole('menuitem', { name: 'Set default value' })).toBeVisible(); + } + if (['editing', 'viewing'].includes(blockType)) { + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + } + if (fieldType === 'relation' && ['creating', 'editing'].includes(blockType)) { + await expect(page.getByRole('menuitem', { name: 'Set default sorting rules' })).toBeVisible(); + } + }); + } +}; + +export async function testDefaultValue({ + page, + openDialog, + closeDialog, + gotoPage, + showMenu, + supportVariables, + constantValue, + variableValue, + inputConstantValue, + expectConstantValue, + expectVariableValue, +}: { + page: Page; + openDialog: () => Promise; + closeDialog: () => Promise; + gotoPage: () => Promise; + showMenu: () => Promise; + /** 支持的变量列表,如:['Current user', 'Date variables', 'Current form'] */ + supportVariables: string[]; + /** 常量默认值 */ + constantValue?: string | number; + /** 变量默认值 */ + variableValue?: string[]; + /** 输入常量默认值 */ + inputConstantValue?: () => Promise; + /** 对常量默认值进行断言 */ + expectConstantValue?: () => Promise; + /** 对变量默认值进行断言 */ + expectVariableValue?: () => Promise; +}) { + await gotoPage(); + await openDialog(); + await showMenu(); + + if (constantValue || inputConstantValue) { + // 设置一个常量作为默认值 + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + if (inputConstantValue) { + await inputConstantValue(); + } else { + await page.getByLabel('block-item-VariableInput-').getByRole('textbox').click(); + await page.getByLabel('block-item-VariableInput-').getByRole('textbox').fill(String(constantValue)); + } + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 关闭弹窗,然后再次打开后,应该显示刚才设置的默认值 + await closeDialog(); + await openDialog(); + await expectConstantValue(); + } + + if (variableValue) { + // 设置一个变量作为默认值 + await showMenu(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + for (const value of supportVariables) { + await expect(page.getByRole('menuitemcheckbox', { name: value })).toBeVisible(); + } + for (const value of variableValue) { + await page.getByRole('menuitemcheckbox', { name: value }).click(); + } + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await closeDialog(); + await openDialog(); + await expectVariableValue(); + } +} + +export async function testPattern({ + page, + gotoPage, + openDialog, + showMenu, + expectEditable, + expectReadonly, + expectEasyReading, +}: { + page: Page; + gotoPage: () => Promise; + openDialog: () => Promise; + showMenu: () => Promise; + /** 断言选项为 Editable 的情况 */ + expectEditable: () => Promise; + /** 断言选项为 Readonly 的情况 */ + expectReadonly: () => Promise; + /** 断言选项为 Easy-reading 的情况 */ + expectEasyReading: () => Promise; +}) { + await gotoPage(); + await openDialog(); + + // 默认情况下,显示的是 Editable + await expectEditable(); + await showMenu(); + await expect(page.getByText('PatternEditable')).toBeVisible(); + + // 更改为 Readonly + await page.getByRole('menuitem', { name: 'Pattern' }).click(); + await page.getByRole('option', { name: 'Readonly' }).click(); + await expectReadonly(); + + // 更改为 Easy-reading + await showMenu(); + await expect(page.getByText('PatternReadonly')).toBeVisible(); + await page.getByRole('menuitem', { name: 'Pattern' }).click(); + await page.getByRole('option', { name: 'Easy-reading' }).click(); + await expectEasyReading(); + + await showMenu(); + await expect(page.getByText('PatternEasy-reading')).toBeVisible(); +} + +export async function testSetValidationRules({ + page, + gotoPage, + openDialog, + showMenu, +}: { + page: Page; + gotoPage: () => Promise; + openDialog: () => Promise; + showMenu: () => Promise; +}) { + await gotoPage(); + await openDialog(); + await showMenu(); + + await page.getByRole('menuitem', { name: 'Set validation rules' }).click(); + await expect(page.getByRole('dialog').getByText('Set validation rules')).toBeVisible(); + + // TODO: 更详细的测试 +} diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/advanced.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/advanced.test.ts new file mode 100644 index 0000000000..4b08773e72 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/advanced.test.ts @@ -0,0 +1,62 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('collection', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'collection', mode: 'options' }); +}); +test.describe('JSON', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'JSON', mode: 'options' }); +}); +test.describe('formula', () => { + test('should display correct options', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'formula' }); + await expect(page.getByRole('menuitem', { name: 'Edit field title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Display title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('sequence', () => { + test('should display correct options', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'sequence' }); + await expect(page.getByRole('menuitem', { name: 'Edit field title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Display title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Edit description' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'required' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/basic.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/basic.test.ts new file mode 100644 index 0000000000..a750ddf28a --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/basic.test.ts @@ -0,0 +1,750 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test } from '@nocobase/test/client'; +import { commonTesting, testDefaultValue, testPattern, testSetValidationRules } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('color', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'color' }); + + test('set default value', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'color' }); + // 简单测试下是否有选项,值的话无法选中,暂时测不了 + await expect(page.getByRole('menuitem', { name: 'Set default value' })).toBeVisible(); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'color'), + expectEditable: async () => { + // 默认情况下显示颜色选择框 + await page.getByLabel('color-picker-normal').hover(); + await expect(page.getByRole('button', { name: 'right Recommended', exact: true })).toBeVisible(); + }, + expectReadonly: async () => { + // 只读模式下,不会显示颜色弹窗 + await page.getByLabel('color-picker-normal').hover(); + await expect(page.getByRole('button', { name: 'right Recommended', exact: true })).not.toBeVisible(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('color-picker-read-pretty')).toBeVisible(); + }, + }); + }); +}); + +test.describe('email', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'email', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'email'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'test@nocobase.com', + variableValue: ['Current user', 'Email'], + expectConstantValue: () => + expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toHaveValue('test@nocobase.com'), + expectVariableValue: () => + expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toHaveValue('admin@nocobase.com'), + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'email'), + expectEditable: async () => { + // 填充一个值,方便后面断言 + await page + .getByLabel('block-item-CollectionField-general-form-general.email-email') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.email-email') + .getByRole('textbox') + .fill('admin@nocobase.com'); + }, + expectReadonly: async () => { + // 输入框应该被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框应该消失,直接显示值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.email-email')).toHaveText( + 'email:admin@nocobase.com', + ); + }, + }); + }); +}); + +test.describe('icon', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'icon', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'icon'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByRole('button', { name: 'Select icon' }).click(); + await page.getByLabel('account-book').locator('svg').click(); + }, + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.icon-icon') + .getByRole('button', { name: 'account-book' }), + ).toBeVisible(); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'icon'), + expectEditable: async () => { + // 默认情况下可以编辑图标 + await page.getByRole('button', { name: 'Select icon' }).click(); + await page.getByLabel('account-book').locator('svg').click(); + }, + expectReadonly: async () => { + // 只读模式下,选择图标按钮会被禁用 + await expect(page.getByRole('button', { name: 'account-book' })).toBeDisabled(); + }, + expectEasyReading: async () => { + // 按钮会消失,只剩下图标 + await expect(page.getByRole('button', { name: 'account-book' })).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.icon-icon').getByLabel('account-book'), + ).toBeVisible(); + }, + }); + }); +}); + +test.describe('single line text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleLineText', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'singleLineText'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'test single line text', + variableValue: ['Current user', 'Email'], // 值为 admin@nocobase.com + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue('test single line text'); + }, + expectVariableValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue('admin@nocobase.com'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleLineText'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .fill('test single line text'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText'), + ).toHaveText('singleLineText:test single line text'); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleLineText'), + }); + }); +}); + +test.describe('integer', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'integer', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'integer'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + variableValue: ['Current user', 'ID'], // 值为 1 + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').click(); + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').fill('112233'); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toHaveValue('112233'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toHaveValue('1'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'integer'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.integer-integer') + .getByRole('spinbutton') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.integer-integer') + .getByRole('spinbutton') + .fill('112233'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.integer-integer')).toHaveText( + 'integer:112233', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'integer'), + }); + }); +}); + +test.describe('number', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'number', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'number'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').click(); + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').fill('11.22'); + }, + variableValue: ['Current user', 'ID'], // 值为 1 + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toHaveValue('11.22'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toHaveValue('1'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + // TODO: number 类型的字段,当输入了小数,然后把 Pattern 切换成 Easy-reading 模式,小数不应该被去掉; + test.fail(); + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'number'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.number-number') + .getByRole('spinbutton') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.number-number') + .getByRole('spinbutton') + .fill('11.22'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.number-number')).toHaveText( + 'number:11.22', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'number'), + }); + }); +}); + +test.describe('password', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'password', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'password'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'test112233password', + variableValue: ['Current user', 'Email'], // 值为 admin@nocobase.com + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).toHaveValue('test112233password'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).toHaveValue('admin@nocobase.com'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'password'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.password-password') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.password-password') + .getByRole('textbox') + .fill('test112233password'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.password-password')).toHaveText( + 'password:********', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'password'), + }); + }); +}); + +test.describe('percent', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'percent', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'percent'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').click(); + await page.getByLabel('block-item-VariableInput-').getByRole('spinbutton').fill('11.22'); + }, + variableValue: ['Current user', 'ID'], // 值为 1 + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).toHaveValue('11.22'); + }, + expectVariableValue: async () => { + // 数字 1 转换为百分比后为 100% + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).toHaveValue('100'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + // TODO: percent 类型的字段,当输入了小数,然后把 Pattern 切换成 Easy-reading 模式,小数点不应该被去掉; + test.fail(); + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'percent'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.percent-percent') + .getByRole('spinbutton') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.percent-percent') + .getByRole('spinbutton') + .fill('11.22'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.percent-percent')).toHaveText( + 'percent:11.22%', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'percent'), + }); + }); +}); + +test.describe('phone', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'phone', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'phone'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: '17777777777', + variableValue: ['Current user', 'ID'], // 值为 1 + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).toHaveValue('17777777777'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).toHaveValue('1'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'phone'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.phone-phone') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.phone-phone') + .getByRole('textbox') + .fill('17777777777'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.phone-phone')).toHaveText( + 'phone:17777777777', + ); + }, + }); + }); +}); + +test.describe('long text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'longText', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: async () => { + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + }, + showMenu: () => showMenu(page, 'longText'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'test long text', + variableValue: ['Current user', 'Email'], // 值为 admin@nocobase.com + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue('test long text'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue('admin@nocobase.com'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'longText'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .fill('test long text'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.longText-longText')).toHaveText( + 'longText:test long text', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'longText'), + }); + }); +}); + +test.describe('URL', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'url', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: async () => { + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + }, + showMenu: () => showMenu(page, 'url'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'https://nocobase.com', + variableValue: ['Current user', 'Email'], // 值为 admin@nocobase.com + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).toHaveValue('https://nocobase.com'); + }, + expectVariableValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).toHaveValue('admin@nocobase.com'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'url'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox').click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.url-url') + .getByRole('textbox') + .fill('https://nocobase.com'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.url-url')).toHaveText( + 'url:https://nocobase.com', + ); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/choices.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/choices.test.ts new file mode 100644 index 0000000000..5f5eacd82c --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/choices.test.ts @@ -0,0 +1,393 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndChoicesFields, test } from '@nocobase/test/client'; +import { commonTesting, testDefaultValue, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndChoicesFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('checkbox', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkbox', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'checkbox'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + // 默认应该是没有被选中的,点击后应该被选中 + await page.getByLabel('block-item-VariableInput-').getByRole('checkbox').click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).toBeChecked(); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'checkbox'), + expectEditable: async () => { + // 默认是未选中的状态,所以先选中 + await page + .getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox') + .getByRole('checkbox') + .click(); + }, + expectReadonly: async () => { + // checkbox 应该被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // checkbox 应该被隐藏,然后只显示一个图标 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).not.toBeVisible(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox') + .getByRole('img', { name: 'check' }), + ).toBeVisible(); + }, + }); + }); +}); + +test.describe('checkbox group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkboxGroup', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'checkboxGroup'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByLabel('Option1').click(); + }, + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }), + ).toBeChecked(); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'checkboxGroup'), + expectEditable: async () => { + // 默认是未选中的状态,所以先选中 + await page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }) + .click(); + }, + expectReadonly: async () => { + // checkbox 应该被禁用 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // checkbox 应该被隐藏,然后只显示一个标签 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }), + ).not.toBeVisible(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByText('Option1'), + ).toBeVisible(); + }, + }); + }); +}); + +test.describe('china region', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'chinaRegion', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'chinaRegion'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByLabel('Search').click(); + await page.getByRole('menuitemcheckbox', { name: '北京市' }).click(); + await page.getByRole('menuitemcheckbox', { name: '市辖区' }).click(); + await page.getByRole('menuitemcheckbox', { name: '东城区' }).click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion'), + ).toHaveText('chinaRegion:北京市/市辖区/东城区'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'chinaRegion'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }) + .click(); + await page.getByRole('menuitemcheckbox', { name: '北京市' }).click(); + await page.getByRole('menuitemcheckbox', { name: '市辖区' }).click(); + await page.getByRole('menuitemcheckbox', { name: '东城区' }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion'), + ).toHaveText('chinaRegion:北京市 / 市辖区 / 东城区'); + }, + }); + }); +}); + +test.describe('multiple select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'multipleSelect', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'multipleSelect'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByTestId('select-multiple').click(); + await page.getByRole('option', { name: 'Option1' }).click(); + await page.getByRole('option', { name: 'Option2' }).click(); + await page.getByRole('option', { name: 'Option3' }).click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect'), + ).toHaveText('multipleSelect:Option1Option2Option3'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'multipleSelect'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple') + .click(); + await page.getByRole('option', { name: 'Option1' }).click(); + await page.getByRole('option', { name: 'Option2' }).click(); + await page.getByRole('option', { name: 'Option3' }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple'), + ).toHaveClass(/ant-select-disabled/); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect'), + ).toHaveText('multipleSelect:Option1Option2Option3'); + }, + }); + }); +}); + +test.describe('radio group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'radioGroup', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'radioGroup'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByLabel('Option2').click(); + }, + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2'), + ).toBeChecked(); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'radioGroup'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2') + .click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup'), + ).toHaveText('radioGroup:Option2'); + }, + }); + }); +}); + +test.describe('single select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleSelect', mode: 'options' }); + + test('set default value', async ({ page, mockPage }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'singleSelect'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByLabel('Search').click(); + await page.getByRole('option', { name: 'Option1' }).click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect'), + ).toHaveText('singleSelect:Option1'); + }, + }); + }); + + test('pattern', async ({ page, mockPage }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleSelect'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single') + .click(); + await page.getByRole('option', { name: 'Option1' }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single'), + ).toHaveClass(/ant-select-disabled/); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect'), + ).toHaveText('singleSelect:Option1'); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/datetime.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/datetime.test.ts new file mode 100644 index 0000000000..dca0f96912 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/datetime.test.ts @@ -0,0 +1,158 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields, test } from '@nocobase/test/client'; +import dayjs from 'dayjs'; +import { commonTesting, testDefaultValue, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('datetime', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'datetime', blockType: 'creating', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'datetime'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByPlaceholder('Select date').click(); + await page.getByText('Today').click(); + }, + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toHaveValue(dayjs().format('YYYY-MM-DD')); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'datetime'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date') + .click(); + await page.getByText('Today').click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.datetime-datetime')).toHaveText( + `datetime:${dayjs().format('YYYY-MM-DD')}`, + ); + }, + }); + }); + + test('date display format', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'datetime' }); + await page.getByRole('menuitem', { name: 'Date display format' }).click(); + await page.getByLabel('YYYY/MM/DD').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 输入一个值,然后验证格式是否正确 + await page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date') + .click(); + await page.getByText('Today').click(); + + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toHaveValue(dayjs().format('YYYY/MM/DD')); + }); +}); +test.describe('time', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'time', blockType: 'creating', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'time'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByPlaceholder('Select time').click(); + await page.getByText('Now').click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.time-time').getByPlaceholder('Select time'), + ).toHaveValue(new RegExp(dayjs().format('HH:mm:'))); // 去掉后面的秒,是因为可能因为延迟导致秒数不一致 + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'time'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.time-time') + .getByPlaceholder('Select time') + .click(); + await page.getByText('Now').click(); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.time-time').getByPlaceholder('Select time'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.time-time')).toHaveText( + new RegExp(`time:${dayjs().format('HH:mm:')}`), + ); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/media.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/media.test.ts new file mode 100644 index 0000000000..4412b9b1a7 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/media.test.ts @@ -0,0 +1,184 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndMediaFields, test } from '@nocobase/test/client'; +import { commonTesting, testDefaultValue, testPattern, testSetValidationRules } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndMediaFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('markdown', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'markdown', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'markdown'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + constantValue: 'test markdown', + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).toHaveValue('test markdown'); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.markdown-markdown') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.markdown-markdown') + .getByRole('textbox') + .fill('test markdown pattern'); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown')).toHaveText( + `markdown:test markdown pattern`, + ); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + }); + }); +}); + +test.describe('rich text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'richText', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'richText'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-CollectionField-general-general.richText').locator('.ql-editor').click(); + await page.keyboard.type('test rich text'); + }, + expectConstantValue: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.richText-richText')).toHaveText( + /test rich text/, + ); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'richText'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.richText-richText') + .locator('.ql-editor') + .click(); + await page.keyboard.type('test rich text pattern'); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.richText-richText').locator('.ql-container'), + ).toHaveClass(/ql-disabled/); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.richText-richText').locator('.ql-container'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.richText-richText')).toHaveText( + /test rich text pattern/, + ); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + }); + }); +}); + +test.describe('attachment', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'attachment', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'attachment'), + expectEditable: async () => { + await expect(page.getByRole('button', { name: 'plus Upload' })).toBeVisible(); + }, + expectReadonly: async () => { + await expect(page.getByRole('button', { name: 'plus Upload' })).not.toBeVisible(); + }, + expectEasyReading: async () => { + await expect(page.getByRole('button', { name: 'plus Upload' })).not.toBeVisible(); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/relation.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/relation.test.ts new file mode 100644 index 0000000000..cce8c12ec7 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/relation.test.ts @@ -0,0 +1,635 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndRelationFields, test } from '@nocobase/test/client'; +import { commonTesting, testDefaultValue, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecords) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndRelationFields).waitForInit(); + await mockRecords('users', 3); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecords, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + mockRecords; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('many to many', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'manyToMany', fieldType: 'relation', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecords }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'manyToMany'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page.getByLabel('block-item-VariableInput-').getByTestId('select-object-multiple').click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + await page.getByRole('option', { name: '2', exact: true }).click(); + await page.getByRole('option', { name: '3', exact: true }).click(); + }, + expectConstantValue: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany'), + ).toHaveText(`manyToMany:123`); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'manyToMany'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple') + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + await page.getByRole('option', { name: '2', exact: true }).click(); + await page.getByRole('option', { name: '3', exact: true }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple'), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany'), + ).toHaveText(`manyToMany:1,2,3`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple') + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + await expect(page.getByRole('option')).toHaveCount(1); + }); + + test('set default sorting rules', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set default sorting rules' }).click(); + + // 配置 + await page.getByRole('button', { name: 'Add sort field' }).click(); + await page.getByTestId('select-single').getByLabel('Search').click(); + await page.getByRole('option', { name: 'ID' }).click(); + await page.getByText('DESC', { exact: true }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set default sorting rules' }).click(); + await expect(page.getByRole('dialog').getByTestId('select-single')).toHaveText('ID'); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + + // 选择 Record picker + await page.getByRole('option', { name: 'Record picker', exact: true }).click(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-data-picker'), + ).toBeVisible(); + + // 选择 Sub-table + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + await page.getByRole('option', { name: 'Sub-table', exact: true }).click(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByLabel('schema-initializer-AssociationField.SubTable-TableColumnInitializers-users'), + ).toBeVisible(); + + // 选择 Sub-form + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + await page.getByRole('option', { name: 'Sub-form', exact: true }).click(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByLabel('schema-initializer-Grid-FormItemInitializers-users'), + ).toBeVisible(); + + // 选择 Sub-form(Popover) + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + await page.getByRole('option', { name: 'Sub-form(Popover)', exact: true }).click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByRole('img', { name: 'edit' }) + .click(); + await expect(page.getByTestId('popover-CollectionField-general')).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('allow multiple', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await expect(page.getByRole('menuitem', { name: 'Allow multiple' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('many to one', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'manyToOne', fieldType: 'relation', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecords }) => { + await testDefaultValue({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + closeDialog: () => page.getByLabel('drawer-Action.Container-general-Add record-mask').click(), + showMenu: () => showMenu(page, 'manyToOne'), + supportVariables: ['Constant', 'Current user', 'Date variables', 'Current form'], + inputConstantValue: async () => { + await page + .getByLabel('block-item-VariableInput-') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + await page + .getByLabel('block-item-VariableInput-') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '2', exact: true }).click(); + await page + .getByLabel('block-item-VariableInput-') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '3', exact: true }).click(); + }, + expectConstantValue: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/), + ).toHaveText(`3`); + }, + }); + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'manyToOne'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne')).toHaveText( + `manyToOne:1`, + ); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByLabel('Search') + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + await expect(page.getByRole('option')).toHaveCount(1); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to many', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'oneToMany', fieldType: 'relation', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName: 'oneToMany' }); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToMany'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple') + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + await page.getByRole('option', { name: '2', exact: true }).click(); + await page.getByRole('option', { name: '3', exact: true }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple'), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany')).toHaveText( + `oneToMany:1,2,3`, + ); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple') + .click(); + // 默认只显示 id 为 1 的数据,因为设置了只过滤 id 为 3 的数据,所以这里的下拉列表应该为空 + await expect(page.getByRole('option')).toHaveCount(0); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('allow multiple', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await expect(page.getByRole('menuitem', { name: 'Allow multiple' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to one (belongs to)', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'oneToOneBelongsTo', fieldType: 'relation', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName: 'oneToOneBelongsTo' }); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToOneBelongsTo'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo'), + ).toHaveText(`oneToOneBelongsTo:1`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByLabel('Search') + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + await expect(page.getByRole('option')).toHaveCount(1); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to one (has one)', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'oneToOneHasOne', fieldType: 'relation', mode: 'options' }); + + test('set default value', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, mockRecords, fieldName: 'oneToOneHasOne' }); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + await testPattern({ + page, + gotoPage: () => gotoPage(mockPage, mockRecords), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToOneHasOne'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByTestId(/select-object/) + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne'), + ).toHaveText(`oneToOneHasOne:1`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByLabel('Search') + .click(); + // 默认只显示 id 为 1 的数据,因为设置了只过滤 id 为 3 的数据,所以这里的下拉列表应该为空 + await expect(page.getByRole('option')).toHaveCount(0); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/systemInfo.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/systemInfo.test.ts new file mode 100644 index 0000000000..5f97200dde --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/creatingForm/systemInfo.test.ts @@ -0,0 +1,81 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields).waitForInit(); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByRole('button', { name: 'Add new' }).click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('created at', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'createdAt', fieldType: 'system', mode: 'options' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdAt' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Date display format' })).toBeVisible(); + }); +}); +test.describe('created by', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'createdBy', fieldType: 'system', mode: 'options' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdBy' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); +test.describe('id', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'id', fieldType: 'system', mode: 'options' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'id' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('table oid', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'tableoid', fieldType: 'system', mode: 'options' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'tableoid' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('last updated at', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'updatedAt', fieldType: 'system', mode: 'options' }); +}); +test.describe('last updated by', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'updatedBy', fieldType: 'system', mode: 'options' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'updatedBy' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/advanced.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/advanced.test.ts new file mode 100644 index 0000000000..6324d0c329 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/advanced.test.ts @@ -0,0 +1,48 @@ +import { Page, oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('collection', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'collection', blockType: 'viewing', mode: 'options' }); +}); +test.describe('JSON', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'JSON', blockType: 'viewing', mode: 'options' }); +}); +test.describe('formula', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'formula', blockType: 'viewing', mode: 'options' }); +}); +test.describe('sequence', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'sequence', blockType: 'viewing', mode: 'options' }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/basic.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/basic.test.ts new file mode 100644 index 0000000000..6a4d0f7079 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/basic.test.ts @@ -0,0 +1,81 @@ +import { Page, oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('color', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'color', blockType: 'viewing' }); +}); + +test.describe('email', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'email', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('icon', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'icon', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('single line text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleLineText', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('integer', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'integer', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('number', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'number', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('password', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'password', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('percent', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'percent', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('phone', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'phone', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('long text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'longText', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('URL', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'url', blockType: 'viewing', mode: 'options' }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/choices.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/choices.test.ts new file mode 100644 index 0000000000..5d90f93eb2 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/choices.test.ts @@ -0,0 +1,65 @@ +import { Page, oneTableBlockWithAddNewAndViewAndEditAndChoicesFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndChoicesFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('checkbox', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkbox', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('checkbox group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkboxGroup', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('china region', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'chinaRegion', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('multiple select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'multipleSelect', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('radio group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'radioGroup', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('single select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleSelect', blockType: 'viewing', mode: 'options' }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/datetime.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/datetime.test.ts new file mode 100644 index 0000000000..45563e4098 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/datetime.test.ts @@ -0,0 +1,59 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields, test } from '@nocobase/test/client'; +import dayjs from 'dayjs'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + const record = await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); + + return record; +}; + +test.describe('datetime', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'datetime', blockType: 'viewing', mode: 'options' }); + + test('date display format', async ({ page, mockPage, mockRecord }) => { + const record = await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'datetime' }); + await page.getByRole('menuitem', { name: 'Date display format' }).click(); + await page.getByLabel('YYYY/MM/DD').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByLabel('block-item-CollectionField-general-form-general.datetime-datetime')).toHaveText( + `datetime:${dayjs(record.datetime).format('YYYY/MM/DD')}`, + ); + }); +}); + +test.describe('time', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'time', blockType: 'viewing', mode: 'options' }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/media.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/media.test.ts new file mode 100644 index 0000000000..c06964495d --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/media.test.ts @@ -0,0 +1,124 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndMediaFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndMediaFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + // 这里是为了等弹窗中的内容渲染稳定后,再去 hover,防止错位导致测试报错 + await page.waitForTimeout(1000); + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('markdown', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'markdown', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('rich text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'richText', blockType: 'viewing', mode: 'options' }); +}); + +test.describe('attachment', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'attachment', blockType: 'viewing', mode: 'options' }); + + test('size', async ({ page, mockPage, mockRecord }) => { + const record = await gotoPage(mockPage, mockRecord); + await openDialog(page); + + // 默认尺寸 + // 这里的尺寸不稳定,所以用 try catch 来处理 + const testDefault = async (value) => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first(), + ).toHaveJSProperty('offsetWidth', value, { timeout: 1000 }); + }; + try { + await testDefault(94); + } catch (error) { + try { + await testDefault(95); + } catch (err) { + await testDefault(96); + } + } + + // 改为大尺寸 + const testLarge = async (value) => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first(), + ).toHaveJSProperty('offsetWidth', value, { timeout: 1000 }); + }; + await showMenu(page, 'attachment'); + await page.getByRole('menuitem', { name: 'Size' }).click(); + await page.getByRole('option', { name: 'Large' }).click(); + try { + await testLarge(153); + } catch (err) { + try { + await testLarge(154); + } catch (err) { + await testLarge(152); + } + } + + // 改为小尺寸 + const testSmall = async (value) => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first(), + ).toHaveJSProperty('offsetWidth', value, { timeout: 1000 }); + }; + await showMenu(page, 'attachment'); + await page.getByRole('menuitem', { name: 'Size' }).click(); + await page.getByRole('option', { name: 'Small' }).click(); + try { + await testSmall(25); + } catch (err) { + try { + await testSmall(26); + } catch (err) { + await testSmall(24); + } + } + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/relation.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/relation.test.ts new file mode 100644 index 0000000000..4c5db02a89 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/relation.test.ts @@ -0,0 +1,256 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndRelationFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecords) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndRelationFields).waitForInit(); + const record = (await mockRecords('general', 1))[0]; + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecords, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + mockRecords; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('many to many', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'manyToMany', + fieldType: 'relation', + mode: 'options', + blockType: 'viewing', + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Title', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Tag', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-details', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); + + test('enable link', async ({ page, mockPage, mockRecords }) => { + test.fail(); + const record = await gotoPage(mockPage, mockRecords); + await openDialog(page); + + // 初始状态是一个可点击的链接 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .locator('a') + .filter({ hasText: record.manyToMany[0].id }), + ).toBeVisible(); + + await showMenu(page, 'manyToMany'); + + // 默认情况下是开启状态 + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + await page.getByRole('menuitem', { name: 'Enable link' }).click(); + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).not.toBeChecked(); + + // 应变为非链接状态 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .locator('a') + .filter({ hasText: record.manyToMany[0].id }), + ).not.toBeVisible(); + + // 再次开启链接状态 + await page.getByRole('menuitem', { name: 'Enable link' }).click(); + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .locator('a') + .filter({ hasText: record.manyToMany[0].id }), + ).toBeVisible(); + }); +}); + +test.describe('many to one', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'manyToOne', + fieldType: 'relation', + mode: 'options', + blockType: 'viewing', + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Title', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Tag', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-details', exact: true })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); + + test('enable link', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + }); +}); + +test.describe('one to many', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToMany', + fieldType: 'relation', + mode: 'options', + blockType: 'viewing', + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Title', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Tag', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-details', exact: true })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); + + test('enable link', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + }); +}); + +test.describe('one to one (belongs to)', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToOneBelongsTo', + fieldType: 'relation', + mode: 'options', + blockType: 'viewing', + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Title', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Tag', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-details', exact: true })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); + + test('enable link', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + }); +}); + +test.describe('one to one (has one)', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToOneHasOne', + fieldType: 'relation', + mode: 'options', + blockType: 'viewing', + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Title', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Tag', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-details', exact: true })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); + + test('enable link', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + + await expect(page.getByRole('menuitem', { name: 'Enable link' }).getByRole('switch')).toBeChecked(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/systemInfo.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/systemInfo.test.ts new file mode 100644 index 0000000000..88f2cc885a --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/details/systemInfo.test.ts @@ -0,0 +1,106 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('created at', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'createdAt', + fieldType: 'system', + mode: 'options', + blockType: 'viewing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdAt' }); + await expect(page.getByRole('menuitem', { name: 'Date display format' })).toBeVisible(); + }); +}); +test.describe('created by', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'createdBy', + fieldType: 'system', + mode: 'options', + blockType: 'viewing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdBy' }); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); +test.describe('id', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'id', fieldType: 'system', mode: 'options', blockType: 'viewing' }); +}); +test.describe('table oid', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'tableoid', + fieldType: 'system', + mode: 'options', + blockType: 'viewing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'tableoid' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('last updated at', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'updatedAt', + fieldType: 'system', + mode: 'options', + blockType: 'viewing', + }); +}); +test.describe('last updated by', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'updatedBy', + fieldType: 'system', + mode: 'options', + blockType: 'viewing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'updatedBy' }); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/advanced.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/advanced.test.ts new file mode 100644 index 0000000000..b43fd1776c --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/advanced.test.ts @@ -0,0 +1,63 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndAdvancedFields).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('collection', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'collection', blockType: 'editing', mode: 'options' }); +}); +test.describe('JSON', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'JSON', blockType: 'editing', mode: 'options' }); +}); +test.describe('formula', () => { + test('should display correct options', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'formula' }); + await expect(page.getByRole('menuitem', { name: 'Edit field title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Display title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('sequence', () => { + test('should display correct options', async ({ page, mockPage, mockRecord, mockRecords }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'sequence' }); + await expect(page.getByRole('menuitem', { name: 'Edit field title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Display title' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Pattern' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Edit description' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'required' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/basic.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/basic.test.ts new file mode 100644 index 0000000000..0d9ffb7298 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/basic.test.ts @@ -0,0 +1,542 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test } from '@nocobase/test/client'; +import { commonTesting, testPattern, testSetValidationRules } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('color', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'color', blockType: 'editing' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'color'), + expectEditable: async () => { + // 默认情况下显示颜色选择框 + await page.getByLabel('color-picker-normal').hover(); + await expect(page.getByRole('button', { name: 'right Recommended', exact: true })).toBeVisible(); + }, + expectReadonly: async () => { + // 只读模式下,不会显示颜色弹窗 + await page.getByLabel('color-picker-normal').hover(); + await expect(page.getByRole('button', { name: 'right Recommended', exact: true })).not.toBeVisible(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('color-picker-read-pretty')).toBeVisible(); + }, + }); + }); +}); + +test.describe('email', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'email', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'email'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toHaveValue(record.email); + }, + expectReadonly: async () => { + // 输入框应该被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框应该消失,直接显示值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.email-email').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.email-email')).toHaveText( + `email:${record.email}`, + ); + }, + }); + }); +}); + +test.describe('icon', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'icon', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'icon'), + expectEditable: async () => { + // 默认情况下可以编辑图标 + await page.getByRole('button', { name: 'Select icon' }).click(); + await page.getByLabel('account-book').locator('svg').click(); + }, + expectReadonly: async () => { + // 只读模式下,选择图标按钮会被禁用 + if (record.icon) { + await expect(page.getByRole('button', { name: record.icon })).toBeDisabled(); + } else { + await expect(page.getByRole('button', { name: 'Select icon' })).toBeDisabled(); + } + }, + expectEasyReading: async () => { + // 按钮会消失,只剩下图标 + if (record.icon) { + await expect(page.getByRole('button', { name: record.icon })).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.icon-icon').getByLabel(record.icon), + ).toBeVisible(); + } else { + await expect(page.getByRole('button', { name: 'Select icon' })).not.toBeVisible(); + } + }, + }); + }); +}); + +test.describe('single line text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleLineText', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleLineText'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue(record.singleLineText); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText'), + ).toHaveText(`singleLineText:${record.singleLineText}`); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleLineText'), + }); + }); +}); + +test.describe('integer', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'integer', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'integer'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toHaveValue(String(record.integer)); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.integer-integer')).toHaveText( + `integer:${record.integer}`, + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'integer'), + }); + }); +}); + +test.describe('number', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'number', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + // TODO: number 类型的字段,当输入了小数,然后把 Pattern 切换成 Easy-reading 模式,小数不应该被去掉; + test.fail(); + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'number'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toHaveValue(String(record.number)); + + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.number-number') + .getByRole('spinbutton') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.number-number') + .getByRole('spinbutton') + .fill('11.22'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.number-number').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.number-number')).toHaveText( + 'number:11.22', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'number'), + }); + }); +}); + +test.describe('password', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'password', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'password'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.password-password') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.password-password') + .getByRole('textbox') + .fill('test112233password'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.password-password').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.password-password')).toHaveText( + 'password:', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'password'), + }); + }); +}); + +test.describe('percent', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'percent', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + // TODO: percent 类型的字段,当输入了小数,然后把 Pattern 切换成 Easy-reading 模式,小数点不应该被去掉; + test.fail(); + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'percent'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).toHaveValue(String(record.percent)); + + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.percent-percent') + .getByRole('spinbutton') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.percent-percent') + .getByRole('spinbutton') + .fill('11.22'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.percent-percent').getByRole('spinbutton'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.percent-percent')).toHaveText( + 'percent:11.22%', + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'percent'), + }); + }); +}); + +test.describe('phone', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'phone', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'phone'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.phone-phone') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.phone-phone') + .getByRole('textbox') + .fill('17777777777'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // 输入框会消失,只剩下值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.phone-phone').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.phone-phone')).toHaveText( + 'phone:', + ); + }, + }); + }); +}); + +test.describe('long text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'longText', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'longText'), + expectEditable: async () => { + // 应该显示 record 中的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue(record.longText); + + // 默认情况下可以编辑 + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .fill('test long text'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.longText-longText')).toHaveText( + `longText:${record.longText}`.replaceAll('\n', ''), + ); + }, + }); + }); + + test('set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'longText'), + }); + }); +}); + +test.describe('URL', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'url', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'url'), + expectEditable: async () => { + // 默认情况下可以编辑 + await page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox').click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.url-url') + .getByRole('textbox') + .fill('https://nocobase.com'); + }, + expectReadonly: async () => { + // 只读模式下,输入框会被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // url 类型数据不会被 mock,所以这里不会显示值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.url-url').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.url-url')).toHaveText('url:'); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/choices.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/choices.test.ts new file mode 100644 index 0000000000..2b743d5205 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/choices.test.ts @@ -0,0 +1,287 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndChoicesFields, test } from '@nocobase/test/client'; +import { commonTesting, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndChoicesFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('checkbox', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkbox', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'checkbox'), + expectEditable: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).toBeChecked({ checked: record.checkbox }); + }, + expectReadonly: async () => { + // checkbox 应该被禁用 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // checkbox 应该被隐藏,然后只显示一个图标 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox').getByRole('checkbox'), + ).not.toBeVisible(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkbox-checkbox') + .getByRole('img', { name: 'check' }), + ).toBeVisible({ visible: record.checkbox }); + }, + }); + }); +}); + +test.describe('checkbox group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'checkboxGroup', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'checkboxGroup'), + expectEditable: async () => { + for (const option of record.checkboxGroup) { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: option }), + ).toBeChecked(); + } + }, + expectReadonly: async () => { + // checkbox 应该被禁用 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + // checkbox 应该被隐藏,然后只显示标签 + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup') + .getByRole('checkbox', { name: 'Option1' }), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.checkboxGroup-checkboxGroup'), + ).toHaveText(`checkboxGroup:${record.checkboxGroup.join('')}`, { ignoreCase: true }); + }, + }); + }); +}); + +test.describe('china region', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'chinaRegion', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'chinaRegion'), + expectEditable: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }) + .click(); + await page.getByRole('menuitemcheckbox', { name: '北京市' }).click(); + await page.getByRole('menuitemcheckbox', { name: '市辖区' }).click(); + await page.getByRole('menuitemcheckbox', { name: '东城区' }).click(); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion') + .getByRole('combobox', { name: 'Search' }), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.chinaRegion-chinaRegion'), + ).toHaveText('chinaRegion:北京市 / 市辖区 / 东城区'); + }, + }); + }); +}); + +test.describe('multiple select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'multipleSelect', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'multipleSelect'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple'), + ).toHaveText(`${record.multipleSelect.join('')}`, { ignoreCase: true }); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple'), + ).toHaveClass(/ant-select-disabled/); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect') + .getByTestId('select-multiple'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.multipleSelect-multipleSelect'), + ).toHaveText(`multipleSelect:${record.multipleSelect.join('')}`, { ignoreCase: true }); + }, + }); + }); +}); + +test.describe('radio group', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'radioGroup', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'radioGroup'), + expectEditable: async () => { + if (record.radioGroup) { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel(record.radioGroup), + ).toBeChecked(); + } + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup') + .getByLabel('Option2'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.radioGroup-radioGroup'), + ).toHaveText(`radioGroup:${record.radioGroup}`, { ignoreCase: true }); + }, + }); + }); +}); + +test.describe('single select', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'singleSelect', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'singleSelect'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single'), + ).toHaveText(record.singleSelect, { ignoreCase: true }); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single'), + ).toHaveClass(/ant-select-disabled/); + }, + expectEasyReading: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect') + .getByTestId('select-single'), + ).not.toBeVisible(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.singleSelect-singleSelect'), + ).toHaveText(`singleSelect:${record.singleSelect}`, { ignoreCase: true }); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/datetime.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/datetime.test.ts new file mode 100644 index 0000000000..8a1ccf283c --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/datetime.test.ts @@ -0,0 +1,119 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields, test } from '@nocobase/test/client'; +import dayjs from 'dayjs'; +import { commonTesting, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndDatetimeFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + const record = await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); + + return record; +}; + +test.describe('datetime', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'datetime', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'datetime'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toHaveValue(dayjs(record.datetime).format('YYYY-MM-DD')); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.datetime-datetime')).toHaveText( + `datetime:${dayjs(record.datetime).format('YYYY-MM-DD')}`, + ); + }, + }); + }); + + test('date display format', async ({ page, mockPage, mockRecord }) => { + const record = await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'datetime' }); + await page.getByRole('menuitem', { name: 'Date display format' }).click(); + await page.getByLabel('YYYY/MM/DD').click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.datetime-datetime') + .getByPlaceholder('Select date'), + ).toHaveValue(dayjs(record.datetime).format('YYYY/MM/DD')); + }); +}); + +test.describe('time', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'time', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'time'), + expectEditable: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.time-time').getByPlaceholder('Select time'), + ).toHaveValue(record.time); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.time-time').getByPlaceholder('Select time'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.time-time')).toHaveText( + new RegExp(`time:${record.time}`), + ); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/media.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/media.test.ts new file mode 100644 index 0000000000..f3c8c5f3e1 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/media.test.ts @@ -0,0 +1,175 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndMediaFields, test } from '@nocobase/test/client'; +import { commonTesting, testPattern, testSetValidationRules } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndMediaFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page + .getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`, { exact: true }) + .hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`, { + exact: true, + }) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('markdown', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'markdown', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + expectEditable: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).toHaveValue(record.markdown); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).toBeDisabled(); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown').getByRole('textbox'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.markdown-markdown')).toHaveText( + `markdown:${record.markdown}`, + ); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + }); + }); +}); + +test.describe('rich text', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'richText', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'richText'), + expectEditable: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.richText-richText').locator('.ql-editor'), + ).toHaveText(record.richText); + }, + expectReadonly: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.richText-richText').locator('.ql-container'), + ).toHaveClass(/ql-disabled/); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.richText-richText').locator('.ql-container'), + ).not.toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-general.richText-richText')).toHaveText( + `richText:${record.richText}`, + ); + }, + }); + }); + + test('Set validation rules', async ({ page, mockPage, mockRecord }) => { + await testSetValidationRules({ + page, + gotoPage: () => gotoPage(mockPage, mockRecord), + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'markdown'), + }); + }); +}); + +test.describe('attachment', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'attachment', blockType: 'editing', mode: 'options' }); + + test('pattern', async ({ page, mockPage, mockRecord }) => { + let record = null; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecord); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'attachment'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first(), + ).toBeVisible(); + }, + expectReadonly: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first() + .hover(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('button', { name: 'delete' }), + ).not.toBeVisible(); + }, + expectEasyReading: async () => { + await page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('link', { name: record.attachment.title }) + .first() + .hover(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.attachment-attachment') + .getByRole('button', { name: 'delete' }), + ).not.toBeVisible(); + }, + }); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/relation.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/relation.test.ts new file mode 100644 index 0000000000..92ad38f0ac --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/relation.test.ts @@ -0,0 +1,567 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndRelationFields, test } from '@nocobase/test/client'; +import { commonTesting, testPattern } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecords) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndRelationFields).waitForInit(); + await mockRecords('users', 3); + const record = (await mockRecords('general', 1))[0]; + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecords, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + mockRecords; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('many to many', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'manyToMany', + fieldType: 'relation', + mode: 'options', + blockType: 'editing', + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + let record; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecords); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'manyToMany'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple'), + ).toHaveText(`${record.manyToMany.map((item) => item.id).join('')}`); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple'), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany'), + ).toHaveText(`manyToMany:${record.manyToMany.map((item) => item.id).join(',')}`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToMany-manyToMany') + .getByTestId('select-object-multiple') + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + }); + + test('set default sorting rules', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set default sorting rules' }).click(); + + // 配置 + await page.getByRole('button', { name: 'Add sort field' }).click(); + await page.getByTestId('select-single').getByLabel('Search').click(); + await page.getByRole('option', { name: 'ID' }).click(); + await page.getByText('DESC', { exact: true }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Set default sorting rules' }).click(); + await expect(page.getByRole('dialog').getByTestId('select-single')).toHaveText('ID'); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('allow multiple', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await expect(page.getByRole('menuitem', { name: 'Allow multiple' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('many to one', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'manyToOne', + fieldType: 'relation', + mode: 'options', + blockType: 'editing', + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + let record; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecords); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'manyToOne'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/), + ).toHaveText(`${record.manyToOne.id}`); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne')).toHaveText( + `manyToOne:${record.manyToOne.id}`, + ); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne') + .getByTestId(/select-object/) + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + await expect(page.getByRole('option')).toHaveCount(2); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'manyToOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to many', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToMany', + fieldType: 'relation', + mode: 'options', + blockType: 'editing', + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + let record; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecords); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToMany'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple'), + ).toHaveText(`${record.oneToMany.map((item) => item.id).join('')}`); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple'), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect(page.getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany')).toHaveText( + `oneToMany:${record.oneToMany.map((item) => item.id).join(',')}`, + ); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + const record = await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany') + .getByTestId('select-object-multiple') + .click(); + // 但是在编辑模式下,本身已经有数据,所以需要加上已有数据的个数 + await expect(page.getByRole('option')).toHaveCount(1 + record.oneToMany.length); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-table', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('allow multiple', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await expect(page.getByRole('menuitem', { name: 'Allow multiple' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToMany'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to one (belongs to)', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToOneBelongsTo', + fieldType: 'relation', + mode: 'options', + blockType: 'editing', + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + let record; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecords); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToOneBelongsTo'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByTestId(/select-object/), + ).toHaveText(`${record.oneToOneBelongsTo.id}`); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo'), + ).toHaveText(`oneToOneBelongsTo:${record.oneToOneBelongsTo.id}`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneBelongsTo-oneToOneBelongsTo') + .getByTestId(/select-object/) + .click(); + await expect(page.getByRole('option', { name: '3', exact: true })).toBeVisible(); + await expect(page.getByRole('option')).toHaveCount(2); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneBelongsTo'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); + +test.describe('one to one (has one)', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'oneToOneHasOne', + fieldType: 'relation', + mode: 'options', + blockType: 'editing', + }); + + test('pattern', async ({ page, mockPage, mockRecords }) => { + let record; + await testPattern({ + page, + gotoPage: async () => { + record = await gotoPage(mockPage, mockRecords); + }, + openDialog: () => openDialog(page), + showMenu: () => showMenu(page, 'oneToOneHasOne'), + expectEditable: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByTestId(/select-object/), + ).toHaveText(`${record.oneToOneHasOne.id}`); + }, + expectReadonly: async () => { + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByTestId(/select-object/), + ).toHaveClass(/ant-select-disabled/); + // 在这里等待一下,防止因闪烁导致下面的断言失败 + await page.waitForTimeout(100); + }, + expectEasyReading: async () => { + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne'), + ).toHaveText(`oneToOneHasOne:${record.oneToOneHasOne.id}`); + }, + }); + }); + + test('Set the data scope', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('3'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 再次打开弹窗,设置的值应该还在 + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await expect(page.getByTestId('select-filter-field')).toHaveText('ID'); + await expect(page.getByRole('spinbutton')).toHaveValue('3'); + await page.getByRole('button', { name: 'Cancel', exact: true }).click(); + + // 数据应该被过滤了 + await page + .getByLabel('block-item-CollectionField-general-form-general.oneToOneHasOne-oneToOneHasOne') + .getByTestId(/select-object/) + .click(); + await expect(page.getByRole('option')).toHaveCount(2); + }); + + test('field component', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + + // 断言支持的选项 + await expect(page.getByRole('option', { name: 'Select', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Record picker', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form', exact: true })).toBeVisible(); + await expect(page.getByRole('option', { name: 'Sub-form(Popover)', exact: true })).toBeVisible(); + }); + + test('quick create', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + + await expect(page.getByRole('menuitem', { name: 'Quick create' })).toBeVisible(); + }); + + test('title field', async ({ page, mockPage, mockRecords }) => { + await gotoPage(mockPage, mockRecords); + await openDialog(page); + await showMenu(page, 'oneToOneHasOne'); + await expect(page.getByRole('menuitem', { name: 'Title field' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/systemInfo.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/systemInfo.test.ts new file mode 100644 index 0000000000..ec0f4ab733 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/formItem/editingForm/systemInfo.test.ts @@ -0,0 +1,114 @@ +import { Page, expect, oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields, test } from '@nocobase/test/client'; +import { commonTesting } from '../commonTesting'; + +const gotoPage = async (mockPage, mockRecord) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndSystemInfoFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + return record; +}; + +const openDialog = async (page: Page) => { + await page.getByLabel('action-Action.Link-Edit record-update-general-table-0').click(); +}; + +const showMenu = async (page: Page, fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); +}; + +const openDialogAndShowMenu = async ({ + page, + mockPage, + mockRecord, + fieldName, +}: { + page: Page; + mockPage; + mockRecord; + fieldName: string; +}) => { + await gotoPage(mockPage, mockRecord); + await openDialog(page); + await showMenu(page, fieldName); +}; + +test.describe('created at', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'createdAt', + fieldType: 'system', + mode: 'options', + blockType: 'editing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdAt' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Date display format' })).toBeVisible(); + }); +}); +test.describe('created by', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'createdBy', + fieldType: 'system', + mode: 'options', + blockType: 'editing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'createdBy' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); +test.describe('id', () => { + commonTesting({ openDialogAndShowMenu, fieldName: 'id', fieldType: 'system', mode: 'options', blockType: 'editing' }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'id' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('table oid', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'tableoid', + fieldType: 'system', + mode: 'options', + blockType: 'editing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'tableoid' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + }); +}); +test.describe('last updated at', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'updatedAt', + fieldType: 'system', + mode: 'options', + blockType: 'editing', + }); +}); +test.describe('last updated by', () => { + commonTesting({ + openDialogAndShowMenu, + fieldName: 'updatedBy', + fieldType: 'system', + mode: 'options', + blockType: 'editing', + }); + + test('options', async ({ page, mockPage, mockRecord }) => { + await openDialogAndShowMenu({ page, mockPage, mockRecord, fieldName: 'updatedBy' }); + await expect(page.getByRole('menuitem', { name: 'Edit tooltip' })).toBeVisible(); + await expect(page.getByRole('menuitem', { name: 'Enable link' })).toBeVisible(); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/linkageRules.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/linkageRules.test.ts new file mode 100644 index 0000000000..bdac3c2713 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/linkageRules.test.ts @@ -0,0 +1,225 @@ +import { expect, oneTableBlockWithAddNewAndViewAndEditAndBasicFields, test } from '@nocobase/test/client'; + +test.describe('LinkageRules', () => { + test('form: basic usage', async ({ page, mockPage }) => { + const openLinkageRules = async () => { + await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + }; + + await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).goto(); + + await page.getByRole('button', { name: 'Add new' }).click(); + + // 设置第一组规则 -------------------------------------------------------------------------- + // 打开联动规则弹窗 + await page.getByLabel('block-item-CardItem-general-form').hover(); + await openLinkageRules(); + + // 增加一组规则 + // 条件:singleLineText 字段的值包含 123 时 + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click(); + await page.getByLabel('Linkage rules').locator('input[type="text"]').click(); + await page.getByLabel('Linkage rules').locator('input[type="text"]').fill('123'); + + // action:禁用 longText 字段 + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByRole('tree').getByText('longText').click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Disabled' }).click(); + + // 保存规则 + await page.getByRole('button', { name: 'OK' }).click(); + + // 验证第一组规则 -------------------------------------------------------------------------- + // 初始状态下,longText 字段是可编辑的 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeEditable(); + + // 输入 123,longText 字段被禁用 + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .fill('123'); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeDisabled(); + + // 清空输入,longText 字段应该恢复成可编辑状态 + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .clear(); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeEditable(); + + // 修改第一组规则,使其条件中包含一个变量 -------------------------------------------------------------------------- + // 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段 + await openLinkageRules(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current form' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'longText' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // singleLineText 字段和 longText 字段都为空的情况下,longText 字段应该是可编辑的 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeEditable(); + + // 先为 longText 字段填入 123,然后为 singleLineText 字段填入 1,longText 字段应该是可编辑的 + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .fill('123'); + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .fill('1'); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeEditable(); + + // 改变变量的值:即将 longText 字段的值改为 1,longText 字段应该是禁用的 + await page + .getByLabel('block-item-CollectionField-general-form-general.longText-longText') + .getByRole('textbox') + .click(); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Backspace'); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeDisabled(); + + // 添加第二组规则 ------------------------------------------------------------------------------------------- + await openLinkageRules(); + + // 增加一条规则:当 number 字段的值等于 123 时 + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click(); + + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add condition', { exact: true }).click(); + await page.getByRole('button', { name: 'Search Select field' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'number' }).click(); + await page.getByLabel('Linkage rules').getByRole('spinbutton').click(); + await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123'); + + // action:使 longText 字段可编辑 + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click(); + await page.getByRole('button', { name: 'Search Select field' }).click(); + await page.getByRole('tree').getByText('longText').click(); + await page.getByRole('button', { name: 'Search action' }).click(); + await page.getByRole('option', { name: 'Editable' }).click(); + + // action: 为 longText 字段赋上常量值 + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click(); + await page.getByRole('button', { name: 'Search Select field' }).click(); + await page.getByRole('tree').getByText('longText').click(); + await page.getByRole('button', { name: 'Search action' }).click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByLabel('dynamic-component-linkage-rules').getByRole('textbox').fill('456'); + + // action: 为 integer 字段附上一个表达式,使其值等于 number 字段的值 + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByText('Add property').click(); + + await page.getByRole('button', { name: 'Search Select field' }).click(); + await page.getByRole('tree').getByText('integer').click(); + await page.getByRole('button', { name: 'Search action' }).click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByTestId('select-linkage-value-type').nth(1).click(); + await page.getByText('Expression').click(); + + await page.getByText('xSelect a variable').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'number' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 验证第二组规则 ------------------------------------------------------------------------------------------- + // 此时 longText 字段是禁用状态,当满足第二组规则时,longText 字段应该是可编辑的 + await page + .getByLabel('block-item-CollectionField-general-form-general.number-number') + .getByRole('spinbutton') + .fill('123'); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toBeEditable(); + + // 并且 longText 字段的值应该是 456 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue('456'); + + // integer 字段的值应该等于 number 字段的值 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.integer-integer').getByRole('spinbutton'), + ).toHaveValue('123'); + }); + + test('action: basic usage', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).waitForInit(); + const record = await mockRecord('general'); + await nocoPage.goto(); + + const openLinkageRules = async () => { + await page.getByLabel('action-Action.Link-View record-view-general-table-0').hover(); + await page.getByRole('button', { name: 'designer-schema-settings-Action.Link-Action.Designer-general' }).hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + }; + + // 设置第一组规则 -------------------------------------------------------------------------- + await openLinkageRules(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + + // 添加一个条件:ID 等于 1 + await page.getByText('Add condition', { exact: true }).click(); + await page.getByTestId('select-filter-field').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID' }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('1'); + + // action: 禁用按钮 + await page.getByText('Add property').click(); + await page.getByLabel('block-item-ArrayCollapse-general').click(); + await page.getByTestId('select-linkage-properties').getByLabel('Search').click(); + await page.getByRole('option', { name: 'Disabled' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await expect(page.getByLabel('action-Action.Link-View record-view-general-table-0')).toHaveAttribute( + 'disabled', + '', + ); + + // 设置第二组规则 -------------------------------------------------------------------------- + await openLinkageRules(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.locator('.ant-collapse-header').nth(1).getByRole('img', { name: 'right' }).click(); + + // 添加一个条件:ID 等于 1 + await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).click(); + await page.getByRole('button', { name: 'Search Select field' }).getByLabel('Search').click(); + await page.getByRole('menuitemcheckbox', { name: 'ID' }).click(); + await page.getByRole('spinbutton').click(); + await page.getByRole('spinbutton').fill('1'); + + // action: 使按钮可用 + await page.getByRole('tabpanel').getByText('Add property').click(); + await page.getByRole('combobox', { name: 'Search' }).click(); + await page.getByRole('option', { name: 'Enabled' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 后面的 action 会覆盖前面的 + await expect(page.getByLabel('action-Action.Link-View record-view-general-table-0')).not.toHaveAttribute( + 'disabled', + '', + ); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/setDefaultValue.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/setDefaultValue.test.ts new file mode 100644 index 0000000000..4918e4b3cc --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/setDefaultValue.test.ts @@ -0,0 +1,204 @@ +import { + expect, + oneTableBlockWithAddNewAndViewAndEditAndBasicFields, + oneTableBlockWithAddNewAndViewAndEditAndRelationFields, + test, +} from '@nocobase/test/client'; + +test.describe('set default value', () => { + test('basic fields', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithAddNewAndViewAndEditAndBasicFields).goto(); + + const openDialog = async (fieldName: string) => { + await page.getByLabel(`block-item-CollectionField-general-form-general.${fieldName}-${fieldName}`).hover(); + await page + .getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-general-general.${fieldName}`) + .hover(); + await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); + }; + + await page.getByRole('button', { name: 'Add new' }).click(); + await openDialog('singleLineText'); + await page.getByLabel('Set default value').getByRole('textbox').click(); + await page.getByLabel('Set default value').getByRole('textbox').fill('test default value'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 关闭弹窗在打开,应该显示默认值 + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + await page.getByRole('button', { name: 'Add new' }).click(); + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue('test default value'); + + // 为 longText 设置一个变量默认值: {{ currentForm.singleLineText }} + await openDialog('longText'); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current form' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + await page.getByRole('button', { name: 'Add new' }).click(); + // 值应该和 singleLineText 一致 + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue('test default value'); + + // 更改变量的值,longText 的值也应该跟着变化 + await page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox') + .fill('new value'); + await expect( + page.getByLabel('block-item-CollectionField-general-form-general.longText-longText').getByRole('textbox'), + ).toHaveValue('new value'); + }); + + test('subform: basic fields', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithAddNewAndViewAndEditAndRelationFields).goto(); + + await page.getByRole('button', { name: 'Add new' }).click(); + + // 先切换为子表单 + await page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-general-general.manyToOne') + .hover(); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + await page.getByRole('option', { name: 'Sub-form', exact: true }).click(); + + // 关闭下拉菜单 + await page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-general-general.manyToOne') + .hover(); + await page.mouse.move(100, 0); + + await page.getByLabel('schema-initializer-Grid-FormItemInitializers-users').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + + // 子表单状态下,没有默认值选项 + await page.getByLabel('block-item-CollectionField-general-form-general.manyToOne-manyToOne').hover(); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + + // 测试子表单字段默认值 ------------------------------------------------------------------------------------------ + await page.getByLabel(`block-item-CollectionField-users-form-users.nickname-Nickname`).hover(); + await page.getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-users-users.Nickname`).hover(); + await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); + await page.getByLabel('Set default value').getByRole('textbox').click(); + await page.getByLabel('Set default value').getByRole('textbox').fill('test default value'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 关闭弹窗在打开,应该显示默认值 + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + await page.getByRole('button', { name: 'Add new' }).click(); + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname').getByRole('textbox'), + ).toHaveValue('test default value'); + + // 为 username 设置一个变量默认值: {{ currentObject.nickname }} + await page.getByLabel(`block-item-CollectionField-users-form-users.username-Username`).hover(); + await page.getByLabel(`designer-schema-settings-CollectionField-FormItem.Designer-users-users.Username`).hover(); + await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current object' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + await page.getByLabel('drawer-Action.Container-general-Add record-mask').click(); + await page.getByRole('button', { name: 'Add new' }).click(); + // 值应该和 Nickname 一致 + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('test default value'); + + // 更改变量的值,longText 的值也应该跟着变化 + await page + .getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname') + .getByRole('textbox') + .fill('new value'); + await expect( + page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'), + ).toHaveValue('new value'); + }); + + test('subtable: basic fields', async ({ page, mockPage }) => { + await mockPage(oneTableBlockWithAddNewAndViewAndEditAndRelationFields).goto(); + + await page.getByRole('button', { name: 'Add new' }).click(); + + // 先切换为子表格 + await page.getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-general-general.oneToMany') + .hover(); + await page.getByRole('menuitem', { name: 'Field component' }).click(); + await page.getByRole('option', { name: 'Sub-table' }).click(); + + // 关闭下拉菜单 + await page.getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-FormItem.Designer-general-general.oneToMany') + .hover(); + await page.mouse.move(100, 0); + + await page.getByLabel('schema-initializer-AssociationField.SubTable-TableColumnInitializers-users').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.getByRole('menuitem', { name: 'Username' }).click(); + + // 子表格状态下,没有默认值选项 + await page.getByLabel('block-item-CollectionField-general-form-general.oneToMany-oneToMany').hover(); + await expect(page.getByRole('menuitem', { name: 'Set default value' })).not.toBeVisible(); + + // 测试子表格字段默认值 ------------------------------------------------------------------------------------------ + await page.getByRole('button', { name: 'Nickname', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-TableV2.Column.Designer-users' }) + .hover(); + await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); + await page.getByLabel('Set default value').getByRole('textbox').click(); + await page.getByLabel('Set default value').getByRole('textbox').fill('test default value'); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 当新增一行时,应该显示默认值 + await page.getByRole('button', { name: 'plus' }).click(); + await expect( + page + .getByRole('cell', { name: 'block-item-CollectionField-users-form-users.nickname-Nickname' }) + .getByRole('textbox'), + ).toHaveValue('test default value'); + + // 为 username 设置一个变量默认值: {{ currentObject.nickname }} + await page.getByRole('button', { name: 'Username', exact: true }).hover(); + await page + .getByRole('button', { name: 'designer-schema-settings-TableV2.Column-TableV2.Column.Designer-users' }) + .hover(); + await page.getByRole('menuitem', { name: 'Set default value', exact: true }).click(); + await page.mouse.move(300, 0); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current object' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + + // 值应该和 Nickname 一致 + await expect( + page + .getByRole('cell', { name: 'block-item-CollectionField-users-form-users.username-Username' }) + .getByRole('textbox'), + ).toHaveValue('test default value'); + + // 更改变量的值,longText 的值也应该跟着变化 + await page + .getByRole('cell', { name: 'block-item-CollectionField-users-form-users.username-Username' }) + .getByRole('textbox') + .fill('new value'); + await expect( + page + .getByRole('cell', { name: 'block-item-CollectionField-users-form-users.username-Username' }) + .getByRole('textbox'), + ).toHaveValue('new value'); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/defaultValue.test.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/defaultValue.test.ts new file mode 100644 index 0000000000..2c693b1c88 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/defaultValue.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@nocobase/test/client'; +import { testingLazyLoadingOfAssociationFieldsForTheCurrentRecord } from './templatesOfPage'; + +test.describe('default value: lazy load value of variables', () => { + test('current form', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(testingLazyLoadingOfAssociationFieldsForTheCurrentRecord).waitForInit(); + await mockRecord('general'); + await nocoPage.goto(); + + await page.getByRole('button', { name: 'Add new' }).click(); + await page + .getByLabel('block-item-CollectionField-general-form-general.m2oField0-m2oField0') + .getByLabel('Search') + .click(); + await page.getByRole('option', { name: '1', exact: true }).click(); + + await expect( + page + .getByLabel('block-item-CollectionField-general-form-general.singleLineText-singleLineText') + .getByRole('textbox'), + ).toHaveValue('1'); + }); +}); diff --git a/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/templatesOfPage.ts b/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/templatesOfPage.ts new file mode 100644 index 0000000000..b99c85a123 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/schemaSettings/variablesLazyLoad/templatesOfPage.ts @@ -0,0 +1,531 @@ +import { PageConfig, generalWithMultiLevelRelationshipFields } from '@nocobase/test/client'; + +export const testingLazyLoadingOfAssociationFieldsForTheCurrentRecord: PageConfig = { + collections: generalWithMultiLevelRelationshipFields, + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + twh6vbpbeh7: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'BlockInitializers', + properties: { + i7hhy9qt3vm: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + zo5ry8mb8i8: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + md9lzbo43sa: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'general:list', + 'x-decorator-props': { + collection: 'general', + resource: 'general', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + disableTemplate: false, + }, + 'x-designer': 'TableBlockDesigner', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'TableActionInitializers', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + properties: { + ztk6e5lq2lu: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-action': 'create', + 'x-acl-action': 'create', + title: "{{t('Add new')}}", + 'x-designer': 'Action.Designer', + 'x-component': 'Action', + 'x-decorator': 'ACLActionProvider', + 'x-component-props': { + openMode: 'drawer', + type: 'primary', + component: 'CreateRecordAction', + icon: 'PlusOutlined', + }, + 'x-align': 'right', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Add record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializersForCreateFormBlock', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'CreateFormBlockInitializers', + properties: { + ec8k6t8hyfd: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + i0kq9r8uagp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + kiwjcmb22ni: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'general:create', + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + resource: 'general', + collection: 'general', + }, + 'x-designer': 'FormV2.Designer', + 'x-component': 'CardItem', + 'x-component-props': {}, + properties: { + q93o4rn1hum: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-component-props': { + useProps: '{{ useFormBlockProps }}', + }, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'FormItemInitializers', + properties: { + '11k61q066eh': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + ilk6g3zfb99: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + m2oField0: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-designer': 'FormItem.Designer', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'general.m2oField0', + 'x-component-props': {}, + 'x-uid': 'zrdwygwt68n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'jbzdmkicrbq', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '19tec55bhv8', + 'x-async': false, + 'x-index': 1, + }, + t964dpznx8s: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + wcc2gxtezpj: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + singleLineText: { + 'x-uid': 'h6nhc1du9zi', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-designer': 'FormItem.Designer', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': + 'general.singleLineText', + 'x-component-props': {}, + default: + '{{$nForm.m2oField0.m2oField1.id}}', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 't1fh9mrbi65', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5pzmjkt6nk9', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'i969ieh0rfu', + 'x-async': false, + 'x-index': 1, + }, + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'CreateFormActionInitializers', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + 'x-uid': 'lrwiyzyg7ow', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'gz3ygly62js', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'q8gn2pt12ng', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p0gdtennahr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'f8741pdnbcy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'f27ge9qf7u6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xa5ujnlaqd6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'd6dy86bjt0b', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'aycw09vymqy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tzdqfr6lmjy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7zzwexd3e4i', + 'x-async': false, + 'x-index': 1, + }, + qoq998l2i65: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'TableColumnInitializers', + 'x-component': 'TableV2', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useProps: '{{ useTableBlockProps }}', + }, + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'TableActionColumnInitializers', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + '8kaku37mdre': { + 'x-uid': 'j1n2bte2wn4', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'View record', + 'x-action': 'view', + 'x-designer': 'Action.Designer', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("View record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'RecordBlockInitializers', + 'x-uid': '6k8q5pvnbjg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'txzczhz6s4g', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'chdb0wf0riz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7jpie07p1s7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + '0xml7nfy4gs': { + 'x-uid': 'inv9i7a8i9t', + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: 'Edit record', + 'x-action': 'update', + 'x-designer': 'Action.Designer', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + danger: false, + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'RecordBlockInitializers', + 'x-uid': 'yqsyvw7utn4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pb3iqfcd6a5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3byo77j4fh6', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '1gscyc9hw6f', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'v4zy14lc4g4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 's2ej5h10jxd', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xoknlt7jnhh', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ovphvqtknwh', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'y4mqu08ojko', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mszhp81fvd9', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ylndxgnumz2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'sj1cs0vneuv', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/application/AppSchemaComponentProvider.tsx b/packages/core/client/src/application/AppSchemaComponentProvider.tsx new file mode 100644 index 0000000000..2576d3beb7 --- /dev/null +++ b/packages/core/client/src/application/AppSchemaComponentProvider.tsx @@ -0,0 +1,35 @@ +import { useLocalStorageState } from 'ahooks'; +import React from 'react'; +import { SchemaComponentProvider } from '../schema-component/core'; +import { ISchemaComponentProvider } from '../schema-component/types'; + +const getKeyByName = (name) => { + if (!name) { + return 'nocobase_designable'.toUpperCase(); + } + return `nocobase_${name}_designable`.toUpperCase(); +}; + +const SchemaComponentProviderWithLocalStorageState: React.FC = ( + props, +) => { + const [designable, setDesignable] = useLocalStorageState(getKeyByName(props.appName), { + defaultValue: props.designable ? true : false, + }); + return ( + { + setDesignable(value); + }} + /> + ); +}; + +export const AppSchemaComponentProvider: React.FC = (props) => { + if (typeof props.designable === 'boolean') { + return ; + } + return ; +}; diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 4cdcb8632e..ec8975838c 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -1,5 +1,5 @@ import { define, observable } from '@formily/reactive'; -import { APIClientOptions } from '@nocobase/sdk'; +import { APIClientOptions, getSubAppName } from '@nocobase/sdk'; import { i18n as i18next } from 'i18next'; import get from 'lodash/get'; import merge from 'lodash/merge'; @@ -9,20 +9,26 @@ import { createRoot } from 'react-dom/client'; import { I18nextProvider } from 'react-i18next'; import { Link, NavLink, Navigate } from 'react-router-dom'; +import { CSSVariableProvider } from '../css-variable'; +import { AntdAppProvider, GlobalThemeProvider } from '../global-theme'; import { PluginManager, PluginType } from './PluginManager'; +import { PluginSettingOptions, PluginSettingsManager } from './PluginSettingsManager'; import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterManager'; import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient'; -import { PluginSettingsManager } from './PluginSettingsManager'; import { APIClient, APIClientProvider } from '../api-client'; import { i18n } from '../i18n'; import { AppComponent, BlankComponent, defaultAppComponents } from './components'; +import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer'; +import * as schemaInitializerComponents from './schema-initializer/components'; +import { SchemaSettings, SchemaSettingsManager } from './schema-settings'; import { compose, normalizeContainer } from './utils'; import { defineGlobalDeps } from './utils/globalDeps'; import { getRequireJs } from './utils/requirejs'; -import type { RequireJS } from './utils/requirejs'; +import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import type { Plugin } from './Plugin'; +import type { RequireJS } from './utils/requirejs'; declare global { interface Window { @@ -33,6 +39,7 @@ declare global { export type DevDynamicImport = (packageName: string) => Promise<{ default: typeof Plugin }>; export type ComponentAndProps = [ComponentType, T]; export interface ApplicationOptions { + name?: string; apiClient?: APIClientOptions; ws?: WebSocketClientOptions | boolean; i18n?: i18next; @@ -41,8 +48,12 @@ export interface ApplicationOptions { components?: Record; scopes?: Record; router?: RouterOptions; - devDynamicImport?: DevDynamicImport; + pluginSettings?: Record; + schemaSettings?: SchemaSettings[]; + schemaInitializers?: SchemaInitializer[]; + designable?: boolean; loadRemotePlugins?: boolean; + devDynamicImport?: DevDynamicImport; } export class Application { @@ -52,18 +63,26 @@ export class Application { public i18n: i18next; public ws: WebSocketClient; public apiClient: APIClient; - public components: Record = { ...defaultAppComponents }; - public pm: PluginManager; + public components: Record | any> = { + ...defaultAppComponents, + ...schemaInitializerComponents, + }; + public pluginManager: PluginManager; public pluginSettingsManager: PluginSettingsManager; public devDynamicImport: DevDynamicImport; public requirejs: RequireJS; public notification; + public schemaInitializerManager: SchemaInitializerManager; + public schemaSettingsManager: SchemaSettingsManager; + + public name: string; + loading = true; maintained = false; maintaining = false; error = null; - get pluginManager() { - return this.pm; + get pm() { + return this.pluginManager; } constructor(protected options: ApplicationOptions = {}) { @@ -84,13 +103,16 @@ export class Application { ...options.router, renderComponent: this.renderComponent.bind(this), }); - this.pm = new PluginManager(options.plugins, options.loadRemotePlugins, this); + this.schemaSettingsManager = new SchemaSettingsManager(options.schemaSettings, this); + this.pluginManager = new PluginManager(options.plugins, options.loadRemotePlugins, this); + this.schemaInitializerManager = new SchemaInitializerManager(options.schemaInitializers, this); this.addDefaultProviders(); this.addReactRouterComponents(); this.addProviders(options.providers || []); this.ws = new WebSocketClient(options.ws); - this.pluginSettingsManager = new PluginSettingsManager(this); + this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this); this.addRoutes(); + this.name = this.options.name || getSubAppName() || 'main'; } private initRequireJs() { @@ -102,6 +124,15 @@ export class Application { private addDefaultProviders() { this.use(APIClientProvider, { apiClient: this.apiClient }); this.use(I18nextProvider, { i18n: this.i18n }); + this.use(GlobalThemeProvider); + this.use(CSSVariableProvider); + this.use(AppSchemaComponentProvider, { + designable: this.options.designable, + appName: this.name, + components: this.components, + scope: this.scopes, + }); + this.use(AntdAppProvider); } private addReactRouterComponents() { @@ -216,7 +247,7 @@ export class Application { return React.createElement(this.getComponent(Component), props); } - addComponent(component: ComponentType, name?: string) { + protected addComponent(component: ComponentType, name?: string) { const componentName = name || component.displayName || component.name; if (!componentName) { console.error('Component must have a displayName or pass name as second argument'); diff --git a/packages/core/client/src/application/Plugin.ts b/packages/core/client/src/application/Plugin.ts index e6306a6618..8dc7b1d82c 100644 --- a/packages/core/client/src/application/Plugin.ts +++ b/packages/core/client/src/application/Plugin.ts @@ -1,11 +1,18 @@ import type { Application } from './Application'; export class Plugin { - constructor(protected options: T, protected app: Application) { + constructor( + protected options: T, + protected app: Application, + ) { this.options = options; this.app = app; } + get pluginManager() { + return this.app.pluginManager; + } + get pm() { return this.app.pm; } @@ -14,6 +21,18 @@ export class Plugin { return this.app.router; } + get pluginSettingsManager() { + return this.app.pluginSettingsManager; + } + + get schemaInitializerManager() { + return this.app.schemaInitializerManager; + } + + get schemaSettingsManager() { + return this.app.schemaSettingsManager; + } + async afterAdd() {} async beforeLoad() {} diff --git a/packages/core/client/src/application/PluginSettingsManager.ts b/packages/core/client/src/application/PluginSettingsManager.ts index a2424974d7..2a77a1858c 100644 --- a/packages/core/client/src/application/PluginSettingsManager.ts +++ b/packages/core/client/src/application/PluginSettingsManager.ts @@ -10,7 +10,7 @@ export const ADMIN_SETTINGS_KEY = 'admin.settings.'; export const ADMIN_SETTINGS_PATH = '/admin/settings/'; export const SNIPPET_PREFIX = 'pm.'; -export interface PluginSettingsManagerSettingOptionsType { +export interface PluginSettingOptions { title: string; /** * @default Outlet @@ -42,11 +42,17 @@ export interface PluginSettingsPageType { } export class PluginSettingsManager { - protected settings: Record = {}; + protected settings: Record = {}; protected aclSnippets: string[] = []; - constructor(protected app: Application) { + constructor( + _pluginSettings: Record, + protected app: Application, + ) { this.app = app; + Object.entries(_pluginSettings || {}).forEach(([name, pluginSettingOptions]) => { + this.add(name, pluginSettingOptions); + }); } setAclSnippets(aclSnippets: string[]) { @@ -66,7 +72,7 @@ export class PluginSettingsManager { return `${ADMIN_SETTINGS_PATH}${name.replaceAll('.', '/')}`; } - add(name: string, options: PluginSettingsManagerSettingOptionsType) { + add(name: string, options: PluginSettingOptions) { const nameArr = name.split('.'); const topLevelName = nameArr[0]; this.settings[name] = { Component: Outlet, ...options, name, topLevelName }; diff --git a/packages/core/client/src/application/RouterManager.tsx b/packages/core/client/src/application/RouterManager.tsx index a8f8ffb32e..f109805e27 100644 --- a/packages/core/client/src/application/RouterManager.tsx +++ b/packages/core/client/src/application/RouterManager.tsx @@ -37,15 +37,7 @@ export class RouterManager { this.options = options || {}; } - setType(type: RouterOptions['type']) { - this.options.type = type; - } - - setBasename(basename: string) { - this.options.basename = basename; - } - - getRoutes(): RouteObject[] { + getRoutesTree(): RouteObject[] { type RouteTypeWithChildren = RouteType & { children?: RouteTypeWithChildren }; const routes: Record = {}; @@ -94,6 +86,18 @@ export class RouterManager { return buildRoutesTree(routes); } + getRoutes() { + return this.routes; + } + + setType(type: RouterOptions['type']) { + this.options.type = type; + } + + setBasename(basename: string) { + this.options.basename = basename; + } + getRouterComponent() { const { type = 'browser', ...opts } = this.options || {}; const Routers = { @@ -105,7 +109,7 @@ export class RouterManager { const ReactRouter = Routers[type]; const RenderRoutes = () => { - const routes = this.getRoutes(); + const routes = this.getRoutesTree(); const element = useRoutes(routes); return element; }; @@ -129,6 +133,14 @@ export class RouterManager { this.routes[name] = route; } + get(name: string) { + return this.routes[name]; + } + + has(name: string) { + return !!this.get(name); + } + remove(name: string) { delete this.routes[name]; } diff --git a/packages/core/client/src/application/__tests__/Application.test.tsx b/packages/core/client/src/application/__tests__/Application.test.tsx index 4944ec9e13..5b428c1ad7 100644 --- a/packages/core/client/src/application/__tests__/Application.test.tsx +++ b/packages/core/client/src/application/__tests__/Application.test.tsx @@ -16,8 +16,7 @@ describe('Application', () => { }); const router: any = { type: 'memory', initialEntries: ['/'] }; - const initialComponentsLength = 7; - const initialProvidersLength = 2; + const initialProvidersLength = 6; it('basic', () => { const app = new Application({ router }); expect(app.i18n).toBeDefined(); @@ -27,8 +26,8 @@ describe('Application', () => { expect(app.providers).toBeDefined(); expect(app.router).toBeDefined(); expect(app.scopes).toBeDefined(); - expect(app.providers).toHaveLength(initialProvidersLength); - expect(Object.keys(app.components)).toHaveLength(initialComponentsLength); + expect(app.providers.length).toBeGreaterThan(1); + expect(Object.keys(app.components).length).toBeGreaterThan(1); }); describe('components', () => { @@ -46,26 +45,6 @@ describe('Application', () => { expect(app.components.Hello).toBe(Hello); }); - it('addComponent', () => { - const app = new Application({ router }); - app.addComponent(Hello); - expect(app.components.Hello).toBe(Hello); - - app.addComponent(Hello, 'Hello2'); - expect(app.components.Hello2).toBe(Hello); - }); - - it('addComponents without name, should error', () => { - const app = new Application({ router }); - - const fn = vitest.fn(); - const originalConsoleError = console.error; - console.error = fn; - app.addComponent(() =>
123
); - expect(fn).toBeCalled(); - console.error = originalConsoleError; - }); - describe('getComponent', () => { let originalConsoleError: any; beforeEach(() => { diff --git a/packages/core/client/src/application/__tests__/RouterManager.test.tsx b/packages/core/client/src/application/__tests__/RouterManager.test.tsx index cdda1ca3be..e44fd6309e 100644 --- a/packages/core/client/src/application/__tests__/RouterManager.test.tsx +++ b/packages/core/client/src/application/__tests__/RouterManager.test.tsx @@ -30,8 +30,8 @@ describe('Router', () => { element:
, }; router.add('test', route1); - expect(router.getRoutes()).toHaveLength(1); - expect(router.getRoutes()).toEqual([route1]); + expect(router.getRoutesTree()).toHaveLength(1); + expect(router.getRoutesTree()).toEqual([route1]); const route2: RouteType = { path: '/test2', @@ -39,8 +39,8 @@ describe('Router', () => { }; router.add('test2', route2); - expect(router.getRoutes()).toHaveLength(2); - expect(router.getRoutes()).toEqual([route1, route2]); + expect(router.getRoutesTree()).toHaveLength(2); + expect(router.getRoutesTree()).toEqual([route1, route2]); }); it('nested route', () => { @@ -55,7 +55,7 @@ describe('Router', () => { router.add('test', route1); router.add('test.test2', route2); - expect(router.getRoutes()).toEqual([{ ...route1, children: [route2] }]); + expect(router.getRoutesTree()).toEqual([{ ...route1, children: [route2] }]); }); it('nested route with empty parent', () => { @@ -75,7 +75,7 @@ describe('Router', () => { router.add('test', route1); router.add('test.empty.test2', route2); router.add('test.empty2.empty3.test3', route3); - expect(router.getRoutes()).toEqual([{ ...route1, children: [route2, route3] }]); + expect(router.getRoutesTree()).toEqual([{ ...route1, children: [route2, route3] }]); }); it('Component', () => { @@ -85,7 +85,7 @@ describe('Router', () => { Component: Hello, }; router.add('test', route); - expect(router.getRoutes()).toEqual([{ path: '/', element: , children: undefined }]); + expect(router.getRoutesTree()).toEqual([{ path: '/', element: , children: undefined }]); }); it('Component is string', () => { @@ -101,7 +101,7 @@ describe('Router', () => { Component: 'Hello', }; router.add('test', route); - expect(router.getRoutes()).toEqual([{ path: '/', element: , children: undefined }]); + expect(router.getRoutesTree()).toEqual([{ path: '/', element: , children: undefined }]); }); }); @@ -117,9 +117,9 @@ describe('Router', () => { element:
, }; router.add('test', route1); - expect(router.getRoutes()).toEqual([route1]); + expect(router.getRoutesTree()).toEqual([route1]); router.remove('test'); - expect(router.getRoutes()).toEqual([]); + expect(router.getRoutesTree()).toEqual([]); }); }); diff --git a/packages/core/client/src/application/__tests__/SettingsCenter.test.ts b/packages/core/client/src/application/__tests__/SettingsCenter.test.ts index bf6b5ede4f..23c58205a1 100644 --- a/packages/core/client/src/application/__tests__/SettingsCenter.test.ts +++ b/packages/core/client/src/application/__tests__/SettingsCenter.test.ts @@ -131,7 +131,7 @@ describe('PluginSettingsManager', () => { it('router', () => { app.pluginSettingsManager.add('test1', test1); app.pluginSettingsManager.add('test1.test2', test2); - expect(app.router.getRoutes()[0]).toMatchInlineSnapshot(` + expect(app.router.getRoutesTree()[0]).toMatchInlineSnapshot(` { "children": undefined, "element": , diff --git a/packages/core/client/src/application/components/AppComponent.tsx b/packages/core/client/src/application/components/AppComponent.tsx index 49d730a918..0b00dd9345 100644 --- a/packages/core/client/src/application/components/AppComponent.tsx +++ b/packages/core/client/src/application/components/AppComponent.tsx @@ -17,13 +17,12 @@ export const AppComponent: FC = observer((props) => { useEffect(() => { app.load(); }, [app]); - + const AppError = app.getComponent('AppError'); if (app.loading) return app.renderComponent('AppSpin', { app }); if (!app.maintained && app.maintaining) return app.renderComponent('AppMaintaining', { app }); - if (app.error?.code === 'LOAD_ERROR') return app.renderComponent('AppError', { app }); - + if (app.error?.code === 'LOAD_ERROR') return ; return ( - ('AppError')} onError={handleErrors}> + } onError={handleErrors}> {app.maintained && app.maintaining && app.renderComponent('AppMaintainingDialog', { app })} {app.renderComponent('AppMain')} diff --git a/packages/core/client/src/application/index.md b/packages/core/client/src/application/index.md index af3e179be8..63d5726607 100644 --- a/packages/core/client/src/application/index.md +++ b/packages/core/client/src/application/index.md @@ -52,9 +52,6 @@ app.addComponents({ Hello, World }); - -// 添加单个组件 -app.addComponent(Hello, 'Hello'); ``` 在插件中添加组件。 diff --git a/packages/core/client/src/application/index.ts b/packages/core/client/src/application/index.ts index 763f99006f..a10d96d436 100644 --- a/packages/core/client/src/application/index.ts +++ b/packages/core/client/src/application/index.ts @@ -3,4 +3,8 @@ export * from './hooks'; export * from './Plugin'; export * from './RouterManager'; export * from './utils'; +export * from './components'; +export * from './schema-initializer'; +export * from './schema-settings'; +export * from './schema-toolbar'; export * from './PluginSettingsManager'; diff --git a/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx b/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx new file mode 100644 index 0000000000..386f541bec --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/SchemaInitializer.tsx @@ -0,0 +1,83 @@ +import { ButtonProps } from 'antd'; +import { SchemaInitializerItemType, SchemaInitializerItemTypeWithoutName, SchemaInitializerOptions } from './types'; + +export class SchemaInitializer { + options: SchemaInitializerOptions; + name: string; + get items() { + return this.options.items; + } + + constructor(options: SchemaInitializerOptions) { + this.options = Object.assign({ items: [] }, options); + this.name = options.name; + } + + add(name: string, item: SchemaInitializerItemTypeWithoutName) { + const arr = name.split('.'); + const data: any = { ...item, name: arr[arr.length - 1] }; + if (arr.length === 1) { + const index = this.items.findIndex((item: any) => item.name === name); + if (index === -1) { + this.items.push(data); + } else { + this.items[index] = data; + } + return; + } + + const parentName = arr.slice(0, -1).join('.'); + const parentItem: any = this.get(parentName); + if (parentItem) { + if (!parentItem.children) { + parentItem.children = []; + } + const index = parentItem.children.findIndex((item: any) => item.name === name); + if (index === -1) { + parentItem.children.push(data); + } else { + parentItem.children[index] = data; + } + } + } + + get(nestedName: string): SchemaInitializerItemType | undefined { + const arr = nestedName.split('.'); + let current: any = this.items; + + for (let i = 0; i < arr.length; i++) { + const name = arr[i]; + current = current.find((item) => item.name === name); + if (!current || i === arr.length - 1) { + return current; + } + + if (current.children) { + current = current.children; + } else { + return undefined; + } + } + + return current; + } + + remove(nestedName: string) { + const arr = nestedName.split('.'); + if (arr.length === 1) { + const index = this.items.findIndex((item) => item.name === arr[0]); + if (index !== -1) { + this.items.splice(index, 1); + } + return; + } + const parent: any = this.get(arr.slice(0, -1).join('.')); + if (parent && parent.children) { + const name = arr[arr.length - 1]; + const index = parent.children.findIndex((item) => item.name === name); + if (index !== -1) { + parent.children.splice(index, 1); + } + } + } +} diff --git a/packages/core/client/src/application/schema-initializer/SchemaInitializerManager.ts b/packages/core/client/src/application/schema-initializer/SchemaInitializerManager.ts new file mode 100644 index 0000000000..e09233f7ac --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/SchemaInitializerManager.ts @@ -0,0 +1,83 @@ +import { ButtonProps } from 'antd'; +import { Application } from '../Application'; +import { SchemaInitializer } from './SchemaInitializer'; +import { SchemaInitializerItemTypeWithoutName } from './types'; + +interface ActionType { + type: 'add' | 'remove'; + itemName: string; + data?: any; +} + +export class SchemaInitializerManager { + protected schemaInitializers: Record> = {}; + protected actionList: Record = {}; + + constructor( + protected _schemaInitializers: SchemaInitializer[] = [], + protected app: Application, + ) { + this.app = app; + + this.add(..._schemaInitializers); + } + + add(...schemaInitializerList: SchemaInitializer[]) { + schemaInitializerList.forEach((schemaInitializer) => { + this.schemaInitializers[schemaInitializer.name] = schemaInitializer; + if (Array.isArray(this.actionList[schemaInitializer.name])) { + this.actionList[schemaInitializer.name].forEach((item) => { + schemaInitializer[item.type](item.itemName, item.data); + }); + this.actionList[schemaInitializer.name] = undefined; + } + }); + } + + addItem(schemaInitializerName: string, itemName: string, data: SchemaInitializerItemTypeWithoutName) { + const schemaInitializer = this.get(schemaInitializerName); + if (!schemaInitializer) { + if (!this.actionList[schemaInitializerName]) { + this.actionList[schemaInitializerName] = []; + } + this.actionList[schemaInitializerName].push({ + type: 'add', + itemName: itemName, + data, + }); + } else { + schemaInitializer.add(itemName, data); + } + } + + get(name: string): SchemaInitializer | undefined { + return this.schemaInitializers[name]; + } + + getAll() { + return this.schemaInitializers; + } + + has(name: string) { + return !!this.get(name); + } + + remove(name: string) { + delete this.schemaInitializers[name]; + } + + removeItem(schemaInitializerName: string, itemName: string) { + const schemaInitializer = this.get(schemaInitializerName); + if (!schemaInitializer) { + if (!this.actionList[schemaInitializerName]) { + this.actionList[schemaInitializerName] = []; + } + this.actionList[schemaInitializerName].push({ + type: 'remove', + itemName: itemName, + }); + } else { + schemaInitializer.remove(itemName); + } + } +} diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx new file mode 100644 index 0000000000..1b2f0210bf --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerActionModal.tsx @@ -0,0 +1,114 @@ +import { useForm } from '@formily/react'; +import React, { FC, useCallback, useMemo } from 'react'; +import { useActionContext, SchemaComponent } from '../../../schema-component'; +import { useSchemaInitializerItem } from '../context'; + +export interface SchemaInitializerActionModalProps { + title: string; + schema: any; + onCancel?: () => void; + onSubmit?: (values: any) => void; + buttonText?: any; + component?: any; +} +export const SchemaInitializerActionModal: FC = (props) => { + const { title, schema, buttonText, component, onCancel, onSubmit } = props; + + const useCancelAction = useCallback(() => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const form = useForm(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const ctx = useActionContext(); + return { + async run() { + await onCancel?.(); + ctx.setVisible(false); + void form.reset(); + }, + }; + }, [onCancel]); + + const useSubmitAction = useCallback(() => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const form = useForm(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const ctx = useActionContext(); + return { + async run() { + await form.validate(); + await onSubmit?.(form.values); + ctx.setVisible(false); + void form.reset(); + }, + }; + }, [onSubmit]); + const defaultSchema = useMemo(() => { + return { + type: 'void', + properties: { + action1: { + type: 'void', + 'x-component': 'Action', + 'x-component-props': component + ? { + component, + } + : { + icon: 'PlusOutlined', + style: { + borderColor: 'var(--colorSettings)', + color: 'var(--colorSettings)', + }, + title: buttonText, + type: 'dashed', + }, + properties: { + drawer1: { + 'x-decorator': 'Form', + 'x-component': 'Action.Modal', + 'x-component-props': { + style: { + maxWidth: '520px', + width: '100%', + }, + }, + type: 'void', + title, + properties: { + ...(schema?.properties || schema), + footer: { + 'x-component': 'Action.Modal.Footer', + type: 'void', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: useCancelAction, + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: useSubmitAction, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + }, [buttonText, component, schema, title, useCancelAction, useSubmitAction]); + + return ; +}; + +export const SchemaInitializerActionModalInternal = () => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerButton.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerButton.tsx new file mode 100644 index 0000000000..4bf5282363 --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerButton.tsx @@ -0,0 +1,35 @@ +import { Button, ButtonProps } from 'antd'; +import React, { FC } from 'react'; +import { Icon } from '../../../icon'; +import { useCompile } from '../../../schema-component'; +import { useGetAriaLabelOfSchemaInitializer } from '../../../schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer'; +import { SchemaInitializerOptions } from '../types'; + +export interface SchemaInitializerButtonProps extends ButtonProps { + options: SchemaInitializerOptions; +} + +export const SchemaInitializerButton: FC = React.memo((props) => { + const { style, options, ...others } = props; + const compile = useCompile(); + const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); + + return ( + + ); +}); +SchemaInitializerButton.displayName = 'SchemaInitializerButton(memo)'; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx new file mode 100644 index 0000000000..da35e2446d --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerChildren.tsx @@ -0,0 +1,81 @@ +import React, { FC, useMemo } from 'react'; + +import { SchemaInitializerItemType } from '../types'; +import { SchemaInitializerItemContext } from '../context'; +import { useFindComponent } from '../../../schema-component'; +export const SchemaInitializerChildren: FC<{ children: SchemaInitializerItemType[] }> = (props) => { + const { children } = props; + if (!children) return null; + return ( + <> + {children + .sort((a, b) => (a.sort || 0) - (b.sort || 0)) + .map((item, index) => ( + + ))} + + ); +}; + +const typeComponentMap: Record = { + item: 'SchemaInitializerItemInternal', + itemGroup: 'SchemaInitializerItemGroupInternal', + divider: 'SchemaInitializerDivider', + subMenu: 'SchemaInitializerSubMenuInternal', + actionModal: 'SchemaInitializerActionModalInternal', +}; + +const useChildrenDefault = () => undefined; +const useVisibleDefault = () => true; +const useComponentPropsDefault = () => undefined; +export const SchemaInitializerChild: FC = (props) => { + const { + type, + Component, + component, + children, + useVisible = useVisibleDefault, + useChildren = useChildrenDefault, + useComponentProps = useComponentPropsDefault, + checkChildrenLength, + componentProps, + sort: _unUse, + ...others + } = props as any; + const useChildrenRes = useChildren(); + const useComponentPropsRes = useComponentProps(); + const findComponent = useFindComponent(); + // 以前的参数是小写 `component`,新的是大写 `Component`,这里做一个兼容 + const componentVal = Component || component; + const isBuiltType = !componentVal && type && typeComponentMap[type]; + + const componentChildren = useChildrenRes || children; + const contextValue = useMemo(() => { + return { + ...others, + ...(isBuiltType ? useComponentPropsRes : {}), + ...(isBuiltType ? componentProps : {}), + children: componentChildren, + }; + }, [others, isBuiltType, useComponentPropsRes, componentProps, componentChildren]); + const visibleResult = useVisible(); + + if (!visibleResult) return null; + if (!type && !componentVal) return null; + + const C = findComponent(isBuiltType ? typeComponentMap[type] : componentVal); + if (!C) { + return null; + } + if (checkChildrenLength && Array.isArray(componentChildren) && componentChildren.length === 0) { + return null; + } + + return ( + + + {componentChildren} + + + ); +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerDivider.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerDivider.tsx new file mode 100644 index 0000000000..0413d813b9 --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerDivider.tsx @@ -0,0 +1,7 @@ +import { Divider, theme } from 'antd'; +import React from 'react'; + +export const SchemaInitializerDivider = () => { + const { token } = theme.useToken(); + return ; +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx new file mode 100644 index 0000000000..a1804c513a --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx @@ -0,0 +1,79 @@ +import { uid } from '@formily/shared'; +import classNames from 'classnames'; +import React, { ReactNode } from 'react'; +import { Icon } from '../../../icon'; +import { useCompile } from '../../../schema-component'; +import { useSchemaInitializerItem } from '../context'; +import { useAriaAttributeOfMenuItem, useSchemaInitializerMenuItems } from '../hooks'; +import { SchemaInitializerMenu } from './SchemaInitializerSubMenu'; +import { useSchemaInitializerStyles } from './style'; +import { MenuProps } from 'antd'; + +export interface SchemaInitializerItemProps { + style?: React.CSSProperties; + className?: string; + name?: string; + icon?: React.ReactNode; + title?: React.ReactNode; + items?: any[]; + onClick?: (args?: any) => any; + applyMenuStyle?: boolean; + children?: ReactNode; +} + +export const SchemaInitializerItem = React.forwardRef((props, ref) => { + const { style, name = uid(), applyMenuStyle = true, className, items, icon, title, onClick, children } = props; + const compile = useCompile(); + const childrenItems = useSchemaInitializerMenuItems(items, name, onClick); + const { componentCls, hashId } = useSchemaInitializerStyles(); + const { attribute } = useAriaAttributeOfMenuItem(); + + if (items && items.length > 0) { + return ( + { + if (info.key !== name) return; + onClick?.({ ...info, item: props }); + }, + icon: typeof icon === 'string' ? : icon, + children: childrenItems, + }, + ]} + > + ); + } + + return ( +
{ + event.stopPropagation(); + onClick?.({ event, item: props }); + }} + > +
+ {children || ( + <> + {icon && typeof icon === 'string' ? : icon} + {compile(title)} + + )} +
+
+ ); +}); + +export const SchemaInitializerItemInternal = () => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx new file mode 100644 index 0000000000..728f3501eb --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItemGroup.tsx @@ -0,0 +1,32 @@ +import { theme } from 'antd'; +import React, { FC } from 'react'; +import { useCompile } from '../../../schema-component'; +import { useSchemaInitializerItem } from '../context'; +import { SchemaInitializerOptions } from '../types'; +import { SchemaInitializerChildren } from './SchemaInitializerChildren'; +import { SchemaInitializerDivider } from './SchemaInitializerDivider'; +import { useSchemaInitializerStyles } from './style'; + +export interface SchemaInitializerItemGroupProps { + title: string; + children?: SchemaInitializerOptions['items']; + divider?: boolean; +} + +export const SchemaInitializerItemGroup: FC = ({ children, title, divider }) => { + const compile = useCompile(); + const { componentCls } = useSchemaInitializerStyles(); + const { token } = theme.useToken(); + return ( +
+ {divider && } +
{compile(title)}
+ {children} +
+ ); +}; + +export const SchemaInitializerItemGroupInternal = () => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItems.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItems.tsx new file mode 100644 index 0000000000..0361cd758e --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItems.tsx @@ -0,0 +1,15 @@ +import { ButtonProps } from 'antd'; +import React, { FC } from 'react'; +import { SchemaInitializerChildren } from './SchemaInitializerChildren'; +import { SchemaInitializerOptions } from '../types'; + +export type SchemaInitializerItemsProps = P2 & { + options?: SchemaInitializerOptions; + items?: SchemaInitializerOptions['items']; +}; + +export const SchemaInitializerItems: FC = (props) => { + const { items } = props; + if (items.length === 0) return null; + return {items}; +}; diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx new file mode 100644 index 0000000000..69d2c19145 --- /dev/null +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSelect.tsx @@ -0,0 +1,58 @@ +import { Select, SelectProps } from 'antd'; +import React, { FC, useCallback, useMemo, useState } from 'react'; +import { SchemaInitializerItemProps, SchemaInitializerItem } from './SchemaInitializerItem'; +import { useSchemaInitializerItem } from '../context'; + +export interface SchemaInitializerSelectItemProps extends SchemaInitializerItemProps { + options?: SelectProps['options']; + value?: SelectProps['defaultValue']; + onChange?: SelectProps['onChange']; + openOnHover?: boolean; +} + +export const SchemaInitializerSelect: FC = (props) => { + const { title, options, value, onChange, openOnHover, onClick: _onClick, ...others } = props; + const [open, setOpen] = useState(false); + + const onClick = (...args) => { + setOpen(false); + _onClick?.(...args); + }; + const onMouseEnter = useCallback(() => setOpen(true), []); + + // 鼠标 hover 时,打开下拉框 + const moreProps = useMemo( + () => + openOnHover + ? { + onMouseEnter, + open, + } + : {}, + [onMouseEnter, open, openOnHover], + ); + + return ( + +
+ {title} + { @@ -15,11 +22,11 @@ export const AssociationFilterBlockDesigner = () => { return ( - - - - - + + + + { - const { t } = useTranslation(); - const associatedFields = useAssociatedFields(); - const optionalList = useOptionalFieldList(); - const useProps = '{{useAssociationFilterBlockProps}}'; - const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ - type: 'item', - key: field.key, - title: field.uiSchema?.title, - component: 'AssociationFilterDesignerDisplayField', - schema: { - name: field.name, - title: field.uiSchema?.title, - type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', - 'x-component': 'AssociationFilter.Item', - 'x-component-props': { - fieldNames: { - label: field.targetKey || 'id', - }, - useProps, +export const associationFilterFilterBlockInitializer = new SchemaInitializer({ + name: 'AssociationFilter.FilterBlockInitializer', + style: { marginTop: 16 }, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'associationFields', + title: '{{t("Association fields")}}', + useChildren() { + const associatedFields = useAssociatedFields(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const children = associatedFields.map((field) => ({ + name: field.key, + title: field.uiSchema?.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema?.title, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.targetKey || 'id', + }, + useProps, + }, + properties: {}, + }, + })); + return children; }, - properties: {}, }, - })); - const optionalChildren: SchemaInitializerItemOptions[] = optionalList.map((field) => ({ - type: 'item', - key: field.key, - title: field.uiSchema.title, - component: 'AssociationFilterDesignerDisplayField', - schema: { - name: field.name, - title: field.uiSchema.title, - interface: field.interface, - type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', - 'x-component': 'AssociationFilter.Item', - 'x-component-props': { - fieldNames: { - label: field.name, - }, - useProps, + { + name: 'choicesFields', + type: 'itemGroup', + title: '{{t("Choices fields")}}', + checkChildrenLength: true, + useChildren() { + const optionalList = useOptionalFieldList(); + const useProps = '{{useAssociationFilterBlockProps}}'; + const optionalChildren = optionalList.map((field) => ({ + name: field.key, + title: field.uiSchema.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema.title, + interface: field.interface, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.name, + }, + useProps, + }, + properties: {}, + }, + })); + + return optionalChildren; }, - properties: {}, }, - })); - - const associatedFieldGroup: SchemaInitializerItemOptions = { - type: 'itemGroup', - title: t('Association fields'), - children, - }; - - // 选项字段 - const optionalFieldGroup: SchemaInitializerItemOptions = { - type: 'itemGroup', - title: t('Choices fields'), - children: optionalChildren, - }; - - const items = [associatedFieldGroup, optionalFieldGroup]; - - return ( - - ); -}; + ], +}); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx index 72d331f5ba..975a104e3b 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx @@ -1,60 +1,54 @@ -import { css } from '@emotion/css'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; +import { SchemaInitializerItemType } from '../../../application'; import { useAssociatedFields } from '../../../filter-provider/utils'; -import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer'; +import { SchemaInitializer } from '../../../application/schema-initializer/SchemaInitializer'; -export const AssociationFilterInitializer = () => { - const { t } = useTranslation(); - const associatedFields = useAssociatedFields(); - const useProps = '{{useAssociationFilterProps}}'; - const children: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ - type: 'item', - key: field.key, - title: field.uiSchema?.title, - component: 'AssociationFilterDesignerDisplayField', - schema: { - name: field.name, - title: field.uiSchema?.title, - type: 'void', - 'x-designer': 'AssociationFilter.Item.Designer', - 'x-component': 'AssociationFilter.Item', - 'x-component-props': { - fieldNames: { - label: field.targetKey || 'id', - }, - useProps, +export const associationFilterInitializer = new SchemaInitializer({ + name: 'AssociationFilter.Initializer', + style: { + marginTop: 16, + }, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + name: 'associationFields', + type: 'itemGroup', + title: '{{t("Association fields")}}', + useChildren() { + const associatedFields = useAssociatedFields(); + const useProps = '{{useAssociationFilterProps}}'; + const children: SchemaInitializerItemType[] = associatedFields.map((field) => ({ + type: 'item', + name: field.key, + title: field.uiSchema?.title, + Component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema?.title, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.targetKey || 'id', + }, + useProps, + }, + properties: {}, + }, + })); + + return children; }, - properties: {}, }, - })); - - const associatedFieldGroup: SchemaInitializerItemOptions = { - type: 'itemGroup', - title: t('Association fields'), - children, - }; - - const dividerItem: SchemaInitializerItemOptions = { - type: 'divider', - }; - - const deleteItem: SchemaInitializerItemOptions = { - type: 'item', - title: t('Delete'), - component: 'AssociationFilterDesignerDelete', - }; - - const items = [associatedFieldGroup, dividerItem, deleteItem]; - - return ( - - ); -}; + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + title: '{{t("Delete")}}', + Component: 'AssociationFilterDesignerDelete', + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx index 3b9ca5325c..cc542a293f 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx @@ -4,7 +4,15 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useFormBlockContext } from '../../../block-provider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; -import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { + GeneralSchemaDesigner, + SchemaSettingsDataScope, + SchemaSettingsDefaultSortingRules, + SchemaSettingsModalItem, + SchemaSettingsRemove, + SchemaSettingsSelectItem, + SchemaSettingsSwitchItem, +} from '../../../schema-settings'; import { useCompile, useDesignable } from '../../hooks'; export const AssociationFilterItemDesigner = (props) => { @@ -49,7 +57,7 @@ export const AssociationFilterItemDesigner = (props) => { return ( - { dn.refresh(); }} /> - { @@ -95,7 +103,7 @@ export const AssociationFilterItemDesigner = (props) => { dn.refresh(); }} /> - { }); }} /> - - + - { const { token } = useToken(); const Designer = useDesigner(); const filedSchema = useFieldSchema(); - const { render } = useSchemaInitializer(filedSchema['x-initializer']); + const { render } = useSchemaInitializerRender(filedSchema['x-initializer'], filedSchema['x-initializer-props']); return ( @@ -85,8 +86,6 @@ export const AssociationFilter = (props) => { }; AssociationFilter.Provider = AssociationFilterProvider; -AssociationFilter.Initializer = AssociationFilterInitializer; -AssociationFilter.FilterBlockInitializer = AssociationFilterFilterBlockInitializer; AssociationFilter.Item = AssociationFilterItem as typeof AssociationFilterItem & { Designer: typeof AssociationFilterItemDesigner; }; @@ -98,3 +97,10 @@ AssociationFilter.useAssociationField = () => { const { getField } = useCollection(); return React.useMemo(() => getField(fieldSchema.name as any), [fieldSchema.name]); }; + +export class AssociationFilterPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(associationFilterFilterBlockInitializer); + this.app.schemaInitializerManager.add(associationFilterInitializer); + } +} diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx index db011d3331..09ec5860cf 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx @@ -1,11 +1,12 @@ import React, { useContext } from 'react'; import { useFieldSchema } from '@formily/react'; -import { SchemaInitializer } from '../../../schema-initializer/SchemaInitializer'; import { createDesignable, SchemaComponentContext } from '../..'; import { useAPIClient } from '../../../api-client'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerItem, useSchemaInitializerItem } from '../../../application'; -export const AssociationFilterDesignerDelete = (props) => { +export const AssociationFilterDesignerDelete = () => { + const itemConfig = useSchemaInitializerItem(); const { refresh } = useContext(SchemaComponentContext); const fieldSchema = useFieldSchema(); const api = useAPIClient(); @@ -18,8 +19,8 @@ export const AssociationFilterDesignerDelete = (props) => { }; return ( - -
{props.title}
-
+ +
{itemConfig.title}
+
); }; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx index bd326556c0..7bc870509f 100644 --- a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx @@ -1,22 +1,24 @@ import { merge } from '@formily/shared'; import React from 'react'; -import { SchemaInitializer } from '../../../schema-initializer'; import { useCurrentSchema } from '../../../schema-initializer/utils'; +import { SchemaInitializerSwitch, useSchemaInitializer, useSchemaInitializerItem } from '../../../application'; -export const AssociationFilterDesignerDisplayField = (props) => { - const { schema, item, insert } = props; - const { exists, remove } = useCurrentSchema(schema.name, 'name', item.find, item.remove); +export const AssociationFilterDesignerDisplayField = () => { + const itemConfig = useSchemaInitializerItem(); + const { schema } = itemConfig; + const { exists, remove } = useCurrentSchema(schema.name, 'name', itemConfig.find, itemConfig.remove); + const { insert } = useSchemaInitializer(); return ( - { if (exists) { return remove(); } - const s = merge(schema || {}, item.schema || {}); - item?.schemaInitialize?.(s); + const s = merge(schema || {}, itemConfig.schema || {}); + itemConfig?.schemaInitialize?.(s); insert(s); }} /> diff --git a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx index cacc06a94c..937a25fc37 100644 --- a/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-select/AssociationSelect.tsx @@ -9,7 +9,16 @@ import { useTranslation } from 'react-i18next'; import { useFilterByTk, useFormBlockContext } from '../../../block-provider'; import { useCollection, useCollectionManager, useSortFields } from '../../../collection-manager'; import { GeneralSchemaItems } from '../../../schema-items'; -import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings'; +import { + GeneralSchemaDesigner, + SchemaSettingsDataScope, + SchemaSettingsDivider, + SchemaSettingsModalItem, + SchemaSettingsRemove, + SchemaSettingsSelectItem, + SchemaSettingsSwitchItem, + isPatternDisabled, +} from '../../../schema-settings'; import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks'; @@ -160,7 +169,7 @@ AssociationSelect.Designer = function Designer() { {form && !form?.readPretty && validateSchema && ( - )} {isAllowToSetDefaultValue() && ( - )} {form && !isSubFormAssociationField && fieldComponentOptions && ( - )} {IsShowMultipleSwitch() ? ( - ) : null} - - {form && !form?.readPretty && !isPatternDisabled(fieldSchema) && ( - )} {collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && ( - )} - {collectionField && } - } + {form && !form?.readPretty && validateSchema && ( - )} {form && !form?.readPretty && collectionField?.uiSchema?.type && ( - )} - - {collectionField?.target && ['CollectionField', 'AssociationSelect'].includes(fieldSchema['x-component']) && ( - )} - {collectionField && } - } + { const fieldSchema = useFieldSchema(); const compile = useCompile(); - const component = fieldSchema['x-component']; + const component = _.isString(fieldSchema['x-component']) + ? fieldSchema['x-component'] + : fieldSchema['x-component']?.displayName; const collectionField = compile(fieldSchema['x-collection-field']); let { name: blockName } = useBlockContext() || {}; // eslint-disable-next-line prefer-const diff --git a/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx b/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx index 32e7ebf298..d47e1a2647 100644 --- a/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/calendar/Calendar.Designer.tsx @@ -5,7 +5,17 @@ import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../. import { useCalendarBlockContext, useFormBlockContext } from '../../../block-provider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; import { RecordProvider, useRecord } from '../../../record-provider'; -import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { + GeneralSchemaDesigner, + SchemaSettingsBlockTitleItem, + SchemaSettingsCascaderItem, + SchemaSettingsDataScope, + SchemaSettingsDivider, + SchemaSettingsRemove, + SchemaSettingsSelectItem, + SchemaSettingsSwitchItem, + SchemaSettingsTemplate, +} from '../../../schema-settings'; import { useSchemaTemplate } from '../../../schema-templates'; export const CalendarDesigner = () => { @@ -25,8 +35,8 @@ export const CalendarDesigner = () => { return ( - - + { dn.refresh(); }} /> - { @@ -63,7 +73,7 @@ export const CalendarDesigner = () => { }} /> - { dn.refresh(); }} /> - { dn.refresh(); }} /> - { }); }} /> - - - - + + + { - it('basic', () => { + it('basic', async () => { render(); - const currentDate = dayjs().format('YYYY-M'); + await waitFor(() => { + const currentDate = dayjs().format('YYYY-M'); - expect(screen.getByText('Today')).toBeInTheDocument(); - expect(screen.getByText(currentDate)).toBeInTheDocument(); - expect(screen.getByText('Month')).toBeInTheDocument(); + expect(screen.getByText('Today')).toBeInTheDocument(); + expect(screen.getByText(currentDate)).toBeInTheDocument(); + expect(screen.getByText('Month')).toBeInTheDocument(); + }); }); - it('use CalendarBlockProvider', () => { + it('use CalendarBlockProvider', async () => { render(); - const currentDate = dayjs().format('YYYY-M'); + await waitFor(() => { + const currentDate = dayjs().format('YYYY-M'); - expect(screen.getByText('Today')).toBeInTheDocument(); - expect(screen.getByText(currentDate)).toBeInTheDocument(); - expect(screen.getByText('Month')).toBeInTheDocument(); + expect(screen.getByText('Today')).toBeInTheDocument(); + expect(screen.getByText(currentDate)).toBeInTheDocument(); + expect(screen.getByText('Month')).toBeInTheDocument(); + }); }); }); diff --git a/packages/core/client/src/schema-component/antd/calendar/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/calendar/demos/demo1.tsx index c8ed698106..227650c1d6 100644 --- a/packages/core/client/src/schema-component/antd/calendar/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/calendar/demos/demo1.tsx @@ -2,10 +2,11 @@ * title: Calendar */ import { + Application, + Plugin, AntdSchemaComponentProvider, SchemaComponent, SchemaComponentProvider, - SchemaInitializerProvider, } from '@nocobase/client'; import React from 'react'; import defaultValues from './defaultValues'; @@ -54,14 +55,32 @@ const schema = { }, }; -export default () => { +const Root = () => { return ( - - - - - + + + ); }; + +class MyPlugin extends Plugin { + async load() { + // 注册路由 + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-component/antd/calendar/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/calendar/demos/demo2.tsx index df706caa51..35e4e5cbbe 100644 --- a/packages/core/client/src/schema-component/antd/calendar/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/calendar/demos/demo2.tsx @@ -3,9 +3,11 @@ import { AntdSchemaComponentProvider, APIClient, APIClientProvider, + Application, BlockSchemaComponentProvider, CollectionManagerProvider, SchemaComponent, + Plugin, SchemaComponentProvider, } from '@nocobase/client'; import MockAdapter from 'axios-mock-adapter'; @@ -100,7 +102,7 @@ const mock = (api: APIClient) => { mock(apiClient); -export default () => { +const Root = () => { return ( @@ -115,3 +117,9 @@ export default () => { ); }; + +const app = new Application({ + providers: [Root], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx b/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx index 335a7bade1..01e292ecd1 100644 --- a/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx +++ b/packages/core/client/src/schema-component/antd/color-picker/ColorPicker.tsx @@ -9,34 +9,36 @@ export const ColorPicker = connect( (props) => { const { value, onChange, ...others } = props; return ( - current} - presets={[ - { - label: 'Recommended', - colors: [ - '#8BBB11', - '#52C41A', - '#13A8A8', - '#1677FF', - '#F5222D', - '#FADB14', - '#FA8C164D', - '#FADB144D', - '#52C41A4D', - '#1677FF4D', - '#2F54EB4D', - '#722ED14D', - '#EB2F964D', - ], - }, - ]} - onChange={(color) => onChange(color.toHexString())} - /> +
+ current} + presets={[ + { + label: 'Recommended', + colors: [ + '#8BBB11', + '#52C41A', + '#13A8A8', + '#1677FF', + '#F5222D', + '#FADB14', + '#FA8C164D', + '#FADB144D', + '#52C41A4D', + '#1677FF4D', + '#2F54EB4D', + '#722ED14D', + '#EB2F964D', + ], + }, + ]} + onChange={(color) => onChange(color.toHexString())} + /> +
); }, mapProps((props, field) => { @@ -48,6 +50,8 @@ export const ColorPicker = connect( const prefixCls = usePrefixCls('description-color-picker', props); return (
{ const { t } = useTranslation(); @@ -13,7 +18,7 @@ export const ExpandActionDesign = (props) => { return ( - { dn.refresh(); }} /> - - + { return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar'); diff --git a/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx b/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx index b37620ee2d..605b6b1d1a 100644 --- a/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/filter/Filter.Action.Designer.tsx @@ -3,7 +3,14 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useDesignable } from '../..'; import { useCollection, useCollectionManager } from '../../../collection-manager'; -import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { + GeneralSchemaDesigner, + SchemaSettingsDivider, + SchemaSettingsItemGroup, + SchemaSettingsModalItem, + SchemaSettingsRemove, + SchemaSettingsSwitchItem, +} from '../../../schema-settings'; import { useCompile } from '../../hooks'; export const useFilterableFields = (collectionName: string) => { @@ -32,11 +39,11 @@ export const FilterActionDesigner = (props) => { const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; return ( - + {fields.map((field) => { const checked = !nonfilterable.includes(field.name); return ( - { /> ); })} - - - + + { dn.refresh(); }} /> - - + { return s['x-component'] === 'Space' || s['x-component'] === 'ActionBar'; diff --git a/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx b/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx index 63c2ed6e73..a7fe50a60b 100644 --- a/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx +++ b/packages/core/client/src/schema-component/antd/filter/FilterItem.tsx @@ -57,7 +57,7 @@ export const FilterItem = observer( { +const Root = () => { return ( @@ -171,3 +172,9 @@ export default () => { ); }; + +const app = new Application({ + providers: [Root], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx index 755b57510e..3cab0f34a1 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormDesigner.tsx @@ -1,45 +1,6 @@ -import { useFieldSchema } from '@formily/react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useCollection, useCollectionManager } from '../../../collection-manager'; -import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; -import { - EditComponent, - EditDescription, - EditOperator, - EditTitle, - EditTitleField, - EditTooltip, - EditValidationRules, -} from './SchemaSettingOptions'; +import { GeneralSchemaDesigner } from '../../../schema-settings'; export const FilterFormDesigner = () => { - const { getCollectionJoinField } = useCollectionManager(); - const { getField } = useCollection(); - const { t } = useTranslation(); - const fieldSchema = useFieldSchema(); - const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); - - return ( - - - - - - - - - {collectionField ? : null} - - - ); + return ; }; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormSettings.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormSettings.tsx new file mode 100644 index 0000000000..612c9313a9 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.FilterFormSettings.tsx @@ -0,0 +1,77 @@ +import { SchemaSettings } from '../../../application/schema-settings'; +import { useFieldSchema } from '@formily/react'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { useCollection, useCollectionManager } from '../../../collection-manager'; +import { + EditComponent, + EditDescription, + EditOperator, + EditTitle, + EditTitleField, + EditTooltip, + EditValidationRules, +} from './SchemaSettingOptions'; + +export const filterFormItemSettings = new SchemaSettings({ + name: 'FilterFormItemSettings', + items: [ + { + name: 'editFieldTitle', + Component: EditTitle, + }, + { + name: 'editDescription', + Component: EditDescription, + }, + { + name: 'editTooltip', + Component: EditTooltip, + }, + { + name: 'validationRules', + Component: EditValidationRules, + }, + { + name: 'fieldMode', + Component: EditComponent, + }, + { + name: 'operator', + Component: EditOperator, + }, + { + name: 'titleField', + Component: EditTitleField, + }, + { + name: 'divider', + type: 'divider', + useVisible() { + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const fieldSchema = useFieldSchema(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + return !!collectionField; + }, + }, + { + name: 'remove', + type: 'remove', + useComponentProps() { + const { t } = useTranslation(); + + return { + removeParentsIfNoChildren: true, + confirm: { + title: t('Delete field'), + }, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx new file mode 100644 index 0000000000..628fb72095 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx @@ -0,0 +1,955 @@ +import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; +import { Field } from '@formily/core'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { Select } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { SchemaSettings } from '../../../application/schema-settings'; +import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; +import { Collection, useCollection, useCollectionManager } from '../../../collection-manager'; +import { useRecord } from '../../../record-provider'; +import { generalSettingsItems } from '../../../schema-items/GeneralSettings'; +import { + SchemaSettingsDataFormat, + SchemaSettingsDataScope, + SchemaSettingsDefaultValue, + SchemaSettingsSortingRule, + isPatternDisabled, +} from '../../../schema-settings'; +import { ActionType } from '../../../schema-settings/LinkageRules/type'; +import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput'; +import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; +import { useLocalVariables, useVariables } from '../../../variables'; +import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; +import { isSubMode } from '../association-field/util'; +import { removeNullCondition } from '../filter'; +import { DynamicComponentProps } from '../filter/DynamicComponent'; +import { getTempFieldState } from '../form-v2/utils'; +import { useColorFields } from '../table-v2/Table.Column.Designer'; + +export const formItemSettings = new SchemaSettings({ + name: 'FormItemSettings', + items: [ + ...(generalSettingsItems as any), + { + name: 'quickUpload', + type: 'switch', + useVisible() { + const isFileField = useIsFileField(); + const isFormReadPretty = useIsFormReadPretty(); + return !isFormReadPretty && isFileField; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Quick upload'), + checked: fieldSchema['x-component-props']?.quickUpload !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.quickUpload = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].quickUpload = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + }, + { + name: 'selectFile', + type: 'switch', + useVisible() { + const isFileField = useIsFileField(); + const isFormReadPretty = useIsFormReadPretty(); + return !isFormReadPretty && isFileField; + }, + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + return { + title: t('Select file'), + checked: fieldSchema['x-component-props']?.selectFile !== (false as boolean), + onChange(value) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.componentProps.selectFile = value; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].selectFile = value; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + }, + { + name: 'validationRules', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + const validateSchema = useValidateSchema(); + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + return { + title: t('Set validation rules'), + components: { ArrayCollapse, FormLayout }, + schema: { + type: 'object', + title: t('Set validation rules'), + properties: { + rules: { + type: 'array', + default: fieldSchema?.['x-validator'], + 'x-component': 'ArrayCollapse', + 'x-decorator': 'FormItem', + 'x-component-props': { + accordion: true, + }, + maxItems: 3, + items: { + type: 'object', + 'x-component': 'ArrayCollapse.CollapsePanel', + 'x-component-props': { + header: '{{ t("Validation rule") }}', + }, + properties: { + index: { + type: 'void', + 'x-component': 'ArrayCollapse.Index', + }, + layout: { + type: 'void', + 'x-component': 'FormLayout', + 'x-component-props': { + labelStyle: { + marginTop: '6px', + }, + labelCol: 8, + wrapperCol: 16, + }, + properties: { + ...validateSchema, + message: { + type: 'string', + title: '{{ t("Error message") }}', + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': { + autoSize: { + minRows: 2, + maxRows: 2, + }, + }, + }, + }, + }, + remove: { + type: 'void', + 'x-component': 'ArrayCollapse.Remove', + }, + moveUp: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveUp', + }, + moveDown: { + type: 'void', + 'x-component': 'ArrayCollapse.MoveDown', + }, + }, + }, + properties: { + add: { + type: 'void', + title: '{{ t("Add validation rule") }}', + 'x-component': 'ArrayCollapse.Addition', + 'x-reactions': { + dependencies: ['rules'], + fulfill: { + state: { + disabled: '{{$deps[0].length >= 3}}', + }, + }, + }, + }, + }, + }, + }, + } as ISchema, + onSubmit(v) { + const rules = []; + for (const rule of v.rules) { + rules.push(_.pickBy(rule, _.identity)); + } + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + if (['percent'].includes(collectionField?.interface)) { + for (const rule of rules) { + if (!!rule.maxValue || !!rule.minValue) { + rule['percentMode'] = true; + } + + if (rule.percentFormat) { + rule['percentFormats'] = true; + } + } + } + const concatValidator = _.concat([], collectionField?.uiSchema?.['x-validator'] || [], rules); + field.validator = concatValidator; + fieldSchema['x-validator'] = rules; + schema['x-validator'] = rules; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const { form } = useFormBlockContext(); + const isFormReadPretty = useIsFormReadPretty(); + const validateSchema = useValidateSchema(); + return form && !isFormReadPretty && validateSchema; + }, + }, + { + name: 'defaultValue', + Component: SchemaSettingsDefaultValue, + useVisible() { + const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue(); + return isAllowToSetDefaultValue(); + }, + }, + { + name: 'dataScope', + Component: SchemaSettingsDataScope, + useVisible() { + const isSelectFieldMode = useIsSelectFieldMode(); + const isFormReadPretty = useIsFormReadPretty(); + return isSelectFieldMode && !isFormReadPretty; + }, + useComponentProps() { + const { getCollectionJoinField, getAllCollectionsInheritChain } = useCollectionManager(); + const { getField } = useCollection(); + const { form } = useFormBlockContext(); + const record = useRecord(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const { dn } = useDesignable(); + return { + collectionName: collectionField?.target, + defaultFilter: fieldSchema?.['x-component-props']?.service?.params?.filter || {}, + form, + dynamicComponent: (props: DynamicComponentProps) => { + return ( + + ); + }, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + _.set(field.componentProps, 'service.params.filter', filter); + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + }, + }; + }, + }, + { + name: 'sortingRule', + Component: SchemaSettingsSortingRule, + useVisible() { + const isSelectFieldMode = useIsSelectFieldMode(); + const isFormReadPretty = useIsFormReadPretty(); + return isSelectFieldMode && !isFormReadPretty; + }, + }, + { + name: 'fieldMode', + type: 'select', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const fieldModeOptions = useFieldModeOptions(); + const isAddNewForm = useIsAddNewForm(); + const fieldMode = useFieldMode(); + + return { + title: t('Field component'), + options: fieldModeOptions, + value: fieldMode, + onChange(mode) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['mode'] = mode; + schema['x-component-props'] = fieldSchema['x-component-props']; + field.componentProps = field.componentProps || {}; + field.componentProps.mode = mode; + + // 子表单状态不允许设置默认值 + if (isSubMode(fieldSchema) && isAddNewForm) { + // @ts-ignore + schema.default = null; + fieldSchema.default = null; + field.setInitialValue(null); + field.setValue(null); + } + + void dn.emit('patch', { + schema, + }); + dn.refresh(); + }, + }; + }, + useVisible: useShowFieldMode, + }, + { + name: 'popupSize', + type: 'item', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + return { + title: t('Popup size'), + children: ( +
+ {t('Popup size')} + { - field.componentProps.openSize = value; - fieldSchema['x-component-props'] = field.componentProps; - dn.emit('patch', { - schema: { - 'x-uid': fieldSchema['x-uid'], - 'x-component-props': fieldSchema['x-component-props'], - }, - }); - dn.refresh(); - }} - style={{ textAlign: 'right', minWidth: 100 }} - /> -
- - )} - {!field.readPretty && isAssociationField && ['Picker'].includes(fieldMode) && ( - { - const hasAddNew = fieldSchema.reduceProperties((buf, schema) => { - if (schema['x-component'] === 'Action') { - return schema; - } - return buf; - }, null); - - if (!hasAddNew) { - const addNewActionschema = { - 'x-action': 'create', - 'x-acl-action': 'create', - title: "{{t('Add new')}}", - 'x-designer': 'Action.Designer', - 'x-component': 'Action', - 'x-decorator': 'ACLActionProvider', - 'x-component-props': { - openMode: 'drawer', - type: 'default', - component: 'CreateRecordAction', - }, - }; - insertAdjacent('afterBegin', addNewActionschema); - } - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - field['x-add-new'] = allowAddNew; - fieldSchema['x-add-new'] = allowAddNew; - schema['x-add-new'] = allowAddNew; - dn.emit('patch', { - schema, - }); - refresh(); - }} - /> - )} - {!field.readPretty && isAssociationField && ['Select'].includes(fieldMode) && ( - { - if (mode === 'modalAdd') { - const hasAddNew = fieldSchema.reduceProperties((buf, schema) => { - if (schema['x-component'] === 'Action') { - return schema; - } - return buf; - }, null); - - if (!hasAddNew) { - const addNewActionschema = { - 'x-action': 'create', - 'x-acl-action': 'create', - title: "{{t('Add new')}}", - 'x-designer': 'Action.Designer', - 'x-component': 'Action', - 'x-decorator': 'ACLActionProvider', - 'x-component-props': { - openMode: 'drawer', - type: 'default', - component: 'CreateRecordAction', - }, - }; - insertAdjacent('afterBegin', addNewActionschema); - } - } - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props']['addMode'] = mode; - schema['x-component-props'] = fieldSchema['x-component-props']; - field.componentProps = field.componentProps || {}; - field.componentProps.addMode = mode; - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> - )} - {isAssociationField && IsShowMultipleSwitch() ? ( - { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - field.componentProps = field.componentProps || {}; - - fieldSchema['x-component-props'].multiple = value; - field.componentProps.multiple = value; - - schema['x-component-props'] = fieldSchema['x-component-props']; - dn.emit('patch', { - schema, - }); - refresh(); - }} - /> - ) : null} - {IsShowMultipleSwitch() && isSubFormMode ? ( - { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - field.componentProps = field.componentProps || {}; - - fieldSchema['x-component-props'].allowDissociate = value; - field.componentProps.allowDissociate = value; - - schema['x-component-props'] = fieldSchema['x-component-props']; - dn.emit('patch', { - schema, - }); - refresh(); - }} - /> - ) : null} - {field.readPretty && options.length > 0 && fieldSchema['x-component'] === 'CollectionField' && !isFileField && ( - { - fieldSchema['x-component-props'] = { - ...fieldSchema?.['x-component-props'], - enableLink: flag, - }; - field.componentProps['enableLink'] = flag; - dn.emit('patch', { - schema: { - 'x-uid': fieldSchema['x-uid'], - 'x-component-props': { - ...fieldSchema?.['x-component-props'], - }, - }, - }); - dn.refresh(); - }} - /> - )} - {form && !form?.readPretty && !isPatternDisabled(fieldSchema) && ( - { - const schema: ISchema = { - ['x-uid']: fieldSchema['x-uid'], - }; - - switch (v) { - case 'readonly': { - fieldSchema['x-read-pretty'] = false; - fieldSchema['x-disabled'] = true; - schema['x-read-pretty'] = false; - schema['x-disabled'] = true; - field.readPretty = false; - field.disabled = true; - _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadOnly)); - break; - } - case 'read-pretty': { - fieldSchema['x-read-pretty'] = true; - fieldSchema['x-disabled'] = false; - schema['x-read-pretty'] = true; - schema['x-disabled'] = false; - field.readPretty = true; - _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadPretty)); - break; - } - default: { - fieldSchema['x-read-pretty'] = false; - fieldSchema['x-disabled'] = false; - schema['x-read-pretty'] = false; - schema['x-disabled'] = false; - field.readPretty = false; - field.disabled = false; - _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.Editable)); - break; - } - } - - dn.emit('patch', { - schema, - }); - - dn.refresh(); - }} - /> - )} - {options.length > 0 && isAssociationField && fieldMode !== 'SubTable' && ( - { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - const fieldNames = { - ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], - ...field.componentProps.fieldNames, - label, - }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props']['fieldNames'] = fieldNames; - schema['x-component-props'] = fieldSchema['x-component-props']; - field.componentProps.fieldNames = fieldSchema['x-component-props'].fieldNames; - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> - )} - {isDateField && } - - {isAttachmentField && field.readPretty && ( - { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props']['size'] = size; - schema['x-component-props'] = fieldSchema['x-component-props']; - field.componentProps = field.componentProps || {}; - field.componentProps.size = size; - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> - )} - - {isAssociationField && ['Tag'].includes(fieldMode) && ( - { - const schema = { - ['x-uid']: fieldSchema['x-uid'], - }; - - fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; - fieldSchema['x-component-props']['tagColorField'] = tagColorField; - schema['x-component-props'] = fieldSchema['x-component-props']; - field.componentProps.tagColorField = tagColorField; - dn.emit('patch', { - schema, - }); - dn.refresh(); - }} - /> - )} - {collectionField && } - -
+ ); }; @@ -732,10 +96,3 @@ export function isFileCollection(collection: Collection) { } FormItem.FilterFormDesigner = FilterFormDesigner; - -function useIsAddNewForm() { - const record = useRecord(); - const isAddNewForm = _.isEmpty(_.omit(record, ['__parent', '__collectionName'])); - - return isAddNewForm; -} diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx index fa5b1e5270..868f92561b 100644 --- a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -7,7 +7,12 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useFormBlockContext } from '../../../block-provider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; -import { SchemaSettings, isPatternDisabled } from '../../../schema-settings'; +import { + SchemaSettingsModalItem, + SchemaSettingsSelectItem, + SchemaSettingsSwitchItem, + isPatternDisabled, +} from '../../../schema-settings'; import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; import { useOperatorList } from '../filter/useOperators'; import { isFileCollection } from './FormItem'; @@ -45,7 +50,7 @@ export const EditTitle = () => { const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); return collectionField ? ( - { const { dn } = useDesignable(); return !field.readPretty ? ( - { const { dn } = useDesignable(); return field.readPretty ? ( - { // TODO: FormField 好像被弃用了,应该删除掉 return !field.readPretty && fieldSchema['x-component'] !== 'FormField' ? ( - { const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema); return form && !form?.readPretty && validateSchema ? ( - { const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); return form && !form?.readPretty && collectionField?.uiSchema?.type ? ( - { const isFileField = isFileCollection(targetCollection as any); return isAssociationField && fieldModeOptions ? ( - { } return form && !form?.readPretty && collectionField?.interface !== 'o2m' && !isPatternDisabled(fieldSchema) ? ( - { } return operatorList.length ? ( - { })); return options.length > 0 && fieldSchema['x-component'] === 'CollectionField' ? ( - { const { name, title } = useCollection(); const template = useSchemaTemplate(); - const fieldSchema = useFieldSchema(); - const defaultResource = fieldSchema?.['x-decorator-props']?.resource; - const { action } = useFormBlockContext(); return ( - - {/* */} - - - {/* 当 action 没有值的时候,说明是在用表单创建新数据,此时需要显示数据模板 */} - {!action ? : null} - - - - - + ); }; export const ReadPrettyFormDesigner = () => { const { name, title } = useCollection(); const template = useSchemaTemplate(); - const fieldSchema = useFieldSchema(); - const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return ( - - {/* */} - - - - - + ); }; export const DetailsDesigner = () => { const { name, title } = useCollection(); const template = useSchemaTemplate(); - const { t } = useTranslation(); - const fieldSchema = useFieldSchema(); - const { form } = useFormBlockContext(); - const field = useField(); - const { service } = useDetailsBlockContext(); - const { dn } = useDesignable(); - const sortFields = useSortFields(name); - const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; - const defaultResource = fieldSchema?.['x-decorator-props']?.resource; - const sort = defaultSort?.map((item: string) => { - return item.startsWith('-') - ? { - field: item.substring(1), - direction: 'desc', - } - : { - field: item, - direction: 'asc', - }; - }); return ( - - - { - filter = removeNullCondition(filter); - const params = field.decoratorProps.params || {}; - params.filter = filter; - field.decoratorProps.params = params; - fieldSchema['x-decorator-props']['params'] = params; - service.run({ ...service.params?.[0], filter }); - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': fieldSchema['x-decorator-props'], - }, - }); - }} - /> - { - const sortArr = sort.map((item) => { - return item.direction === 'desc' ? `-${item.field}` : item.field; - }); - const params = field.decoratorProps.params || {}; - params.sort = sortArr; - field.decoratorProps.params = params; - fieldSchema['x-decorator-props']['params'] = params; - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-decorator-props': fieldSchema['x-decorator-props'], - }, - }); - service.run({ ...service.params?.[0], sort: sortArr }); - }} - /> - - - - + ); }; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.FilterDesigner.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.FilterDesigner.tsx index a0def045cf..abc67779d8 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.FilterDesigner.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.FilterDesigner.tsx @@ -1,9 +1,6 @@ -import { useFieldSchema } from '@formily/react'; import React from 'react'; -import { useTranslation } from 'react-i18next'; import { useCollection } from '../../../collection-manager'; -import { FilterBlockType } from '../../../filter-provider/utils'; -import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { GeneralSchemaDesigner } from '../../../schema-settings'; import { useSchemaTemplate } from '../../../schema-templates'; /** @@ -13,27 +10,12 @@ import { useSchemaTemplate } from '../../../schema-templates'; export const FilterDesigner = () => { const { name, title } = useCollection(); const template = useSchemaTemplate(); - const fieldSchema = useFieldSchema(); - const { t } = useTranslation(); - const defaultResource = fieldSchema?.['x-decorator-props']?.resource; return ( - - - - - - - - + ); }; diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx new file mode 100644 index 0000000000..990110c98c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.FilterSettings.tsx @@ -0,0 +1,70 @@ +import { useFieldSchema } from '@formily/react'; +import { SchemaSettings } from '../../../application/schema-settings'; +import { useCollection } from '../../../collection-manager'; +import { FilterBlockType } from '../../../filter-provider'; +import { useTranslation } from 'react-i18next'; +import { + SchemaSettingsFormItemTemplate, + SchemaSettingsLinkageRules, + SchemaSettingsConnectDataBlocks, + SchemaSettingsBlockTitleItem, +} from '../../../schema-settings'; + +export const formFilterSettings = new SchemaSettings({ + name: 'FormFilterSettings', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'FilterFormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection(); + return { + collectionName: name, + }; + }, + }, + { + name: 'connectDataBlocks', + Component: SchemaSettingsConnectDataBlocks, + useComponentProps() { + const { t } = useTranslation(); + return { + type: FilterBlockType.FORM, + emptyDescription: t('No blocks to connect'), + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx new file mode 100644 index 0000000000..939c38d82f --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.Settings.tsx @@ -0,0 +1,308 @@ +import { ArrayItems } from '@formily/antd-v5'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +import { useFormBlockContext } from '../../../block-provider'; +import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider'; +import { useCollection } from '../../../collection-manager'; +import { useSortFields } from '../../../collection-manager/action-hooks'; +import { useDesignable } from '../../hooks'; +import { removeNullCondition } from '../filter'; +import { SchemaSettings } from '../../../application/schema-settings'; +import { + SchemaSettingsLinkageRules, + SchemaSettingsDataTemplates, + SchemaSettingsFormItemTemplate, + SchemaSettingsDataScope, + SchemaSettingsBlockTitleItem, +} from '../../../schema-settings'; + +export const formSettings = new SchemaSettings({ + name: 'FormSettings', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { name } = useCollection(); + return { + collectionName: name, + }; + }, + }, + { + name: 'dataTemplates', + Component: SchemaSettingsDataTemplates, + useVisible() { + const { action } = useFormBlockContext(); + return !action; + }, + useComponentProps() { + const { name } = useCollection(); + return { + collectionName: name, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'FormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider2', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); + +export const readPrettyFormSettings = new SchemaSettings({ + name: 'ReadPrettyFormSettings', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + insertAdjacentPosition: 'beforeEnd', + componentName: 'ReadPrettyFormItem', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); + +export const formDetailsSettings = new SchemaSettings({ + name: 'FormDetailsSettings', + items: [ + { + name: 'title', + Component: SchemaSettingsBlockTitleItem, + }, + { + name: 'dataScope', + Component: SchemaSettingsDataScope, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const { form } = useFormBlockContext(); + const field = useField(); + const { service } = useDetailsBlockContext(); + const { dn } = useDesignable(); + return { + collectionName: name, + defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {}, + form, + onSubmit: ({ filter }) => { + filter = removeNullCondition(filter); + const params = field.decoratorProps.params || {}; + params.filter = filter; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + service.run({ ...service.params?.[0], filter }); + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + }, + }; + }, + }, + { + name: 'sortingRules', + type: 'modal', + useComponentProps() { + const { name } = useCollection(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { service } = useDetailsBlockContext(); + const { dn } = useDesignable(); + const sortFields = useSortFields(name); + const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; + const sort = defaultSort?.map((item: string) => { + return item.startsWith('-') + ? { + field: item.substring(1), + direction: 'desc', + } + : { + field: item, + direction: 'asc', + }; + }); + return { + title: t('Set default sorting rules'), + components: { + ArrayItems, + }, + schema: { + type: 'object', + title: t('Set default sorting rules'), + properties: { + sort: { + type: 'array', + default: sort, + 'x-component': 'ArrayItems', + 'x-decorator': 'FormItem', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + field: { + type: 'string', + enum: sortFields, + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + 'x-component-props': { + style: { + width: 260, + }, + }, + }, + direction: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + 'x-component-props': { + optionType: 'button', + }, + enum: [ + { + label: t('ASC'), + value: 'asc', + }, + { + label: t('DESC'), + value: 'desc', + }, + ], + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: t('Add sort field'), + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + } as ISchema, + onSubmit({ sort }) { + const sortArr = sort.map((item) => { + return item.direction === 'desc' ? `-${item.field}` : item.field; + }); + const params = field.decoratorProps.params || {}; + params.sort = sortArr; + field.decoratorProps.params = params; + fieldSchema['x-decorator-props']['params'] = params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + service.run({ ...service.params?.[0], sort: sortArr }); + }, + }; + }, + }, + { + name: 'formItemTemplate', + Component: SchemaSettingsFormItemTemplate, + useComponentProps() { + const { name } = useCollection(); + const fieldSchema = useFieldSchema(); + const defaultResource = fieldSchema?.['x-decorator-props']?.resource; + return { + componentName: 'Details', + collectionName: name, + resourceName: defaultResource, + }; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx index 34160ca9e8..ce3d7de641 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Templates.tsx @@ -157,6 +157,9 @@ export const Templates = ({ style = {}, form }) => { { - onChange(e.target.value); - setValue(e.target.value); - }} - /> - -
- ); -}; diff --git a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx index a813c87baa..090bc73bdc 100644 --- a/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/BlockInitializers.tsx @@ -1,103 +1,87 @@ import { gridRowColWrap } from '../utils'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -// 页面里添加区块 -export const BlockInitializers = { +export const blockInitializers = new SchemaInitializer({ + name: 'BlockInitializers', title: '{{t("Add block")}}', icon: 'PlusOutlined', wrap: gridRowColWrap, items: [ { - key: 'dataBlocks', - type: 'itemGroup', + name: 'dataBlocks', title: '{{t("Data blocks")}}', - children: [ - { - key: 'table', - type: 'item', - title: '{{t("Table")}}', - component: 'TableBlockInitializer', - }, - { - key: 'form', - type: 'item', - title: '{{t("Form")}}', - component: 'FormBlockInitializer', - }, - { - key: 'details', - type: 'item', - title: '{{t("Details")}}', - component: 'DetailsBlockInitializer', - }, - { - key: 'List', - type: 'item', - title: '{{t("List")}}', - component: 'ListBlockInitializer', - }, - { - key: 'GridCard', - type: 'item', - title: '{{t("Grid Card")}}', - component: 'GridCardBlockInitializer', - }, - { - key: 'calendar', - type: 'item', - title: '{{t("Calendar")}}', - component: 'CalendarBlockInitializer', - }, - { - key: 'kanban', - type: 'item', - title: '{{t("Kanban")}}', - component: 'KanbanBlockInitializer', - }, - { - key: 'Gantt', - type: 'item', - title: '{{t("Gantt")}}', - component: 'GanttBlockInitializer', - }, - ], - }, - { - key: 'filterBlocks', type: 'itemGroup', - title: '{{t("Filter blocks")}}', children: [ { - key: 'filterForm', - type: 'item', - title: '{{t("Form")}}', - component: 'FilterFormBlockInitializer', + name: 'table', + title: '{{t("Table")}}', + Component: 'TableBlockInitializer', }, { - key: 'filterCollapse', - type: 'item', - title: '{{t("Collapse")}}', - component: 'FilterCollapseBlockInitializer', + name: 'form', + title: '{{t("Form")}}', + Component: 'FormBlockInitializer', + }, + { + name: 'details', + title: '{{t("Details")}}', + Component: 'DetailsBlockInitializer', + }, + { + name: 'list', + title: '{{t("List")}}', + Component: 'ListBlockInitializer', + }, + { + name: 'gridCard', + title: '{{t("Grid Card")}}', + Component: 'GridCardBlockInitializer', + }, + { + name: 'calendar', + title: '{{t("Calendar")}}', + Component: 'CalendarBlockInitializer', + }, + { + name: 'kanban', + title: '{{t("Kanban")}}', + Component: 'KanbanBlockInitializer', + }, + { + name: 'gantt', + title: '{{t("Gantt")}}', + Component: 'GanttBlockInitializer', }, ], }, { - key: 'media', + name: 'filterBlocks', + title: '{{t("Filter blocks")}}', + type: 'itemGroup', + children: [ + { + name: 'filterForm', + title: '{{t("Form")}}', + Component: 'FilterFormBlockInitializer', + }, + { + name: 'filterCollapse', + title: '{{t("Collapse")}}', + Component: 'FilterCollapseBlockInitializer', + }, + ], + }, + { + name: 'otherBlocks', type: 'itemGroup', title: '{{t("Other blocks")}}', children: [ { - key: 'markdown', - type: 'item', + name: 'markdown', title: '{{t("Markdown")}}', - component: 'MarkdownBlockInitializer', - }, - { - key: 'auditLogs', - type: 'item', - title: '{{t("Audit logs")}}', - component: 'AuditLogsBlockInitializer', + Component: 'MarkdownBlockInitializer', }, ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/BulkEditFormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/BulkEditFormItemInitializers.tsx index c7b7becb90..6a9f3a73fb 100644 --- a/packages/core/client/src/schema-initializer/buttons/BulkEditFormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/BulkEditFormItemInitializers.tsx @@ -1,60 +1,36 @@ -import { union } from 'lodash'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../SchemaInitializer'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap, useCustomBulkEditFormItemInitializerFields } from '../utils'; -export const BulkEditFormItemInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component } = props; - return ( - ( - [ - { - type: 'itemGroup', - title: t('Display fields'), - children: useCustomBulkEditFormItemInitializerFields(), - }, - ], - // associationFields.length > 0 - // ? [ - // { - // type: 'divider', - // }, - // { - // type: 'itemGroup', - // title: t('Display association fields'), - // children: associationFields, - // }, - // ] - // : [], - [ - { - type: 'divider', - }, - { - type: 'item', - title: t('Add text'), - component: 'BlockInitializer', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'FormItem', - 'x-designer': 'Markdown.Void.Designer', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: t('This is a demo text, **supports Markdown syntax**.'), - }, - }, - }, - ], - )} - insertPosition={insertPosition} - component={component} - title={component ? null : t('Configure fields')} - /> - ); -}; +export const bulkEditFormItemInitializers = new SchemaInitializer({ + name: 'BulkEditFormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: '{{t("Display fields")}}', + useChildren: useCustomBulkEditFormItemInitializerFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'addText', + title: '{{t("Add text")}}', + Component: 'BlockItemInitializer', + schema: { + type: 'void', + 'x-editable': false, + 'x-decorator': 'FormItem', + 'x-designer': 'Markdown.Void.Designer', + 'x-component': 'Markdown.Void', + 'x-component-props': { + content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx index f5cc74d417..8ea1628d4c 100644 --- a/packages/core/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CalendarActionInitializers.tsx @@ -1,7 +1,9 @@ import { useCollection } from '../../'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; // 日历的操作配置 -export const CalendarActionInitializers = { +export const calendarActionInitializers = new SchemaInitializer({ + name: 'CalendarActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', style: { marginLeft: 8 }, @@ -9,11 +11,12 @@ export const CalendarActionInitializers = { { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'today', title: '{{t("Today")}}', - component: 'ActionInitializer', + Component: 'ActionInitializer', schema: { title: '{{t("Today")}}', 'x-component': 'CalendarV2.Today', @@ -22,9 +25,9 @@ export const CalendarActionInitializers = { }, }, { - type: 'item', + name: 'turnPages', title: '{{t("Turn pages")}}', - component: 'ActionInitializer', + Component: 'ActionInitializer', schema: { title: '{{t("Turn pages")}}', 'x-component': 'CalendarV2.Nav', @@ -33,9 +36,9 @@ export const CalendarActionInitializers = { }, }, { - type: 'item', + name: 'title', title: '{{t("Title")}}', - component: 'ActionInitializer', + Component: 'ActionInitializer', schema: { title: '{{t("Title")}}', 'x-component': 'CalendarV2.Title', @@ -44,9 +47,9 @@ export const CalendarActionInitializers = { }, }, { - type: 'item', + name: 'selectView', title: '{{t("Select view")}}', - component: 'ActionInitializer', + Component: 'ActionInitializer', schema: { title: '{{t("Select view")}}', 'x-component': 'CalendarV2.ViewSelect', @@ -56,17 +59,17 @@ export const CalendarActionInitializers = { }, }, { - type: 'item', + name: 'filter', title: "{{t('Filter')}}", - component: 'FilterActionInitializer', + Component: 'FilterActionInitializer', schema: { 'x-align': 'right', }, }, { - type: 'item', + name: 'addNew', title: '{{ t("Add new") }}', - component: 'CreateActionInitializer', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -74,7 +77,7 @@ export const CalendarActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, @@ -82,4 +85,4 @@ export const CalendarActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx index 695e61cc21..7428967bbe 100644 --- a/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CalendarFormActionInitializers.tsx @@ -1,7 +1,9 @@ import { useCollection } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; // 表单的操作配置 -export const CalendarFormActionInitializers = { +export const calendarFormActionInitializers = new SchemaInitializer({ + name: 'CalendarFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', style: { @@ -10,12 +12,13 @@ export const CalendarFormActionInitializers = { items: [ { type: 'itemGroup', + name: 'enableActions', title: '{{t("Enable actions")}}', children: [ { - type: 'item', + name: 'edit', title: '{{t("Edit")}}', - component: 'UpdateActionInitializer', + Component: 'UpdateActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', @@ -23,59 +26,60 @@ export const CalendarFormActionInitializers = { type: 'primary', }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'delete', title: '{{t("Delete")}}', - component: 'DestroyActionInitializer', + Component: 'DestroyActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'deleteEvent', title: '{{t("Delete Event")}}', - component: 'DeleteEventActionInitializer', + Component: 'DeleteEventActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'print', title: '{{t("Print")}}', - component: 'PrintActionInitializer', + Component: 'PrintActionInitializer', schema: { 'x-component': 'Action', }, }, ], }, - { + name: 'divider', type: 'divider', }, { type: 'subMenu', + name: 'customize', title: '{{t("Customize")}}', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -123,9 +127,9 @@ export const CalendarFormActionInitializers = { }, }, { - type: 'item', + name: 'updateRecord', title: '{{t("Update record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Update record") }}', 'x-component': 'Action', @@ -144,16 +148,16 @@ export const CalendarFormActionInitializers = { useProps: '{{ useCustomizeUpdateActionProps }}', }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', - visible: function useVisible() { + Component: 'CustomRequestInitializer', + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, @@ -161,4 +165,4 @@ export const CalendarFormActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx index 12acb733e7..e03027eae5 100644 --- a/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CreateFormBlockInitializers.tsx @@ -1,42 +1,35 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; -export const CreateFormBlockInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component } = props; - return ( - - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CreateFormBulkEditBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CreateFormBulkEditBlockInitializers.tsx index 8a4cce1bf2..3efccec67d 100644 --- a/packages/core/client/src/schema-initializer/buttons/CreateFormBulkEditBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CreateFormBulkEditBlockInitializers.tsx @@ -1,42 +1,35 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; -export const CreateFormBulkEditBlockInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component } = props; - return ( - - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx index bd5cb4dc8a..54f358f9ed 100644 --- a/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CusomeizeCreateFormBlockInitializers.tsx @@ -1,43 +1,36 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; -export const CusomeizeCreateFormBlockInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component } = props; - return ( - - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/CustomFormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/CustomFormItemInitializers.tsx index 0c3375cdc8..24c1c295ad 100644 --- a/packages/core/client/src/schema-initializer/buttons/CustomFormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/CustomFormItemInitializers.tsx @@ -1,45 +1,44 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerChildren } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCompile } from '../../schema-component'; -import { SchemaInitializer } from '../SchemaInitializer'; import { gridRowColWrap, useCustomFormItemInitializerFields, useInheritsFormItemInitializerFields } from '../utils'; // 表单里配置字段 -export const CustomFormItemInitializers = (props: any) => { +const ParentCollectionFields = () => { + const inheritFields = useInheritsFormItemInitializerFields({ component: 'AssignedField' }); const { t } = useTranslation(); const compile = useCompile(); - const { insertPosition, component } = props; - const inheritFields = useInheritsFormItemInitializerFields({ component: 'AssignedField' }); - const fieldItems: any[] = [ + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + return {res}; +}; + +export const customFormItemInitializers = new SchemaInitializer({ + name: 'CustomFormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ { type: 'itemGroup', - title: t('Configure fields'), - children: useCustomFormItemInitializerFields(), + title: '{{t("Configure fields")}}', + name: 'configureFields', + useChildren: useCustomFormItemInitializerFields, }, - ]; - if (inheritFields?.length > 0) { - inheritFields.forEach((inherit) => { - Object.values(inherit)[0].length && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0], - }, - ); - }); - } - return ( - - ); -}; + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx index 7aea1f4005..7ddfc2426a 100644 --- a/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/DetailsActionInitializers.tsx @@ -1,5 +1,8 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; + // 表单的操作配置 -export const DetailsActionInitializers = { +export const detailsActionInitializers = new SchemaInitializer({ + name: 'DetailsActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', style: { @@ -9,11 +12,12 @@ export const DetailsActionInitializers = { { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'edit', title: '{{t("Edit")}}', - component: 'UpdateActionInitializer', + Component: 'UpdateActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', @@ -23,18 +27,18 @@ export const DetailsActionInitializers = { }, }, { - type: 'item', + name: 'delete', title: '{{t("Delete")}}', - component: 'DestroyActionInitializer', + Component: 'DestroyActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', }, }, { - type: 'item', + name: 'duplicate', title: '{{t("Duplicate")}}', - component: 'DuplicateActionInitializer', + Component: 'DuplicateActionInitializer', schema: { 'x-component': 'Action', 'x-action': 'duplicate', @@ -47,4 +51,4 @@ export const DetailsActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx index d41d84a02a..340fcd56a8 100644 --- a/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/FilterFormActionInitializers.tsx @@ -1,24 +1,28 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; + // 表单的操作配置 -export const FilterFormActionInitializers = { +export const filterFormActionInitializers = new SchemaInitializer({ + name: 'FilterFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'filter', title: '{{t("Filter")}}', - component: 'CreateFilterActionInitializer', + Component: 'CreateFilterActionInitializer', schema: { 'x-action-settings': {}, }, }, { - type: 'item', + name: 'reset', title: '{{t("Reset")}}', - component: 'CreateResetActionInitializer', + Component: 'CreateResetActionInitializer', schema: { 'x-action-settings': {}, }, @@ -26,4 +30,4 @@ export const FilterFormActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx index 6179772fc7..65868c4ee4 100644 --- a/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/FormActionInitializers.tsx @@ -1,8 +1,11 @@ +import { SchemaInitializerItemType } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; + // TODO(refactor): should be moved to workflow plugin -const FormTriggerWorkflowActionInitializer = { - type: 'item', +const formTriggerWorkflowActionInitializerV2: SchemaInitializerItemType = { + name: 'submitToWorkflow', title: '{{t("Submit to workflow", { ns: "workflow" })}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{t("Submit to workflow", { ns: "workflow" })}}', 'x-component': 'Action', @@ -25,18 +28,20 @@ const FormTriggerWorkflowActionInitializer = { }; // 表单的操作配置 -export const FormActionInitializers = { +export const formActionInitializers = new SchemaInitializer({ + name: 'FormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', + name: 'enableActions', title: '{{t("Enable actions")}}', children: [ { - type: 'item', + name: 'submit', title: '{{t("Submit")}}', - component: 'CreateSubmitActionInitializer', + Component: 'CreateSubmitActionInitializer', schema: { 'x-action-settings': {}, }, @@ -44,67 +49,18 @@ export const FormActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { + name: 'custom', type: 'subMenu', title: '{{t("Customize")}}', children: [ - // 表单区块内暂时屏蔽【打开弹窗】按钮 - // { - // type: 'item', - // title: '{{t("Popup")}}', - // component: 'CustomizeActionInitializer', - // schema: { - // type: 'void', - // title: '{{ t("Popup") }}', - // 'x-action': 'customize:popup', - // 'x-designer': 'Action.Designer', - // 'x-component': 'Action', - // 'x-component-props': { - // openMode: 'drawer', - // }, - // properties: { - // drawer: { - // type: 'void', - // title: '{{ t("Popup") }}', - // 'x-component': 'Action.Container', - // 'x-component-props': { - // className: 'nb-action-popup', - // }, - // properties: { - // tabs: { - // type: 'void', - // 'x-component': 'Tabs', - // 'x-component-props': {}, - // 'x-initializer': 'TabPaneInitializers', - // properties: { - // tab1: { - // type: 'void', - // title: '{{t("Details")}}', - // 'x-component': 'Tabs.TabPane', - // 'x-designer': 'Tabs.Designer', - // 'x-component-props': {}, - // properties: { - // grid: { - // type: 'void', - // 'x-component': 'Grid', - // 'x-initializer': 'RecordBlockInitializers', - // properties: {}, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, { - type: 'item', + name: 'saveRecord', title: '{{t("Save record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Save record") }}', 'x-action': 'customize:save', @@ -129,29 +85,31 @@ export const FormActionInitializers = { }, }, }, - FormTriggerWorkflowActionInitializer, + formTriggerWorkflowActionInitializerV2, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', }, ], }, ], -}; +}); -export const CreateFormActionInitializers = { +export const createFormActionInitializers = new SchemaInitializer({ + name: 'CreateFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'submit', title: '{{t("Submit")}}', - component: 'CreateSubmitActionInitializer', + Component: 'CreateSubmitActionInitializer', schema: { 'x-action-settings': {}, }, @@ -159,67 +117,18 @@ export const CreateFormActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { type: 'subMenu', title: '{{t("Customize")}}', + name: 'customize', children: [ - // 添加弹窗内暂时屏蔽【打开弹窗】按钮 - // { - // type: 'item', - // title: '{{t("Popup")}}', - // component: 'CustomizeActionInitializer', - // schema: { - // type: 'void', - // title: '{{ t("Popup") }}', - // 'x-action': 'customize:popup', - // 'x-designer': 'Action.Designer', - // 'x-component': 'Action', - // 'x-component-props': { - // openMode: 'drawer', - // }, - // properties: { - // drawer: { - // type: 'void', - // title: '{{ t("Popup") }}', - // 'x-component': 'Action.Container', - // 'x-component-props': { - // className: 'nb-action-popup', - // }, - // properties: { - // tabs: { - // type: 'void', - // 'x-component': 'Tabs', - // 'x-component-props': {}, - // 'x-initializer': 'TabPaneInitializers', - // properties: { - // tab1: { - // type: 'void', - // title: '{{t("Details")}}', - // 'x-component': 'Tabs.TabPane', - // 'x-designer': 'Tabs.Designer', - // 'x-component-props': {}, - // properties: { - // grid: { - // type: 'void', - // 'x-component': 'Grid', - // 'x-initializer': 'RecordBlockInitializers', - // properties: {}, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, - // }, { - type: 'item', + name: 'saveRecord', title: '{{t("Save record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Save record") }}', 'x-action': 'customize:save', @@ -244,29 +153,31 @@ export const CreateFormActionInitializers = { }, }, }, - FormTriggerWorkflowActionInitializer, + formTriggerWorkflowActionInitializerV2, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', }, ], }, ], -}; +}); -export const UpdateFormActionInitializers = { +export const updateFormActionInitializers = new SchemaInitializer({ + name: 'UpdateFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'submit', title: '{{t("Submit")}}', - component: 'UpdateSubmitActionInitializer', + Component: 'UpdateSubmitActionInitializer', schema: { 'x-action-settings': {}, }, @@ -274,16 +185,18 @@ export const UpdateFormActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { type: 'subMenu', title: '{{t("Customize")}}', + name: 'customize', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -331,9 +244,9 @@ export const UpdateFormActionInitializers = { }, }, { - type: 'item', + name: 'saveRecord', title: '{{t("Save record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Save") }}', 'x-component': 'Action', @@ -358,29 +271,32 @@ export const UpdateFormActionInitializers = { }, }, }, - FormTriggerWorkflowActionInitializer, + formTriggerWorkflowActionInitializerV2, { type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', }, ], }, ], -}; +}); -export const BulkEditFormActionInitializers = { +export const bulkEditFormActionInitializers = new SchemaInitializer({ + name: 'BulkEditFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enableActions', children: [ { - type: 'item', + name: 'submit', title: '{{t("Submit")}}', - component: 'BulkEditSubmitActionInitializer', + Component: 'BulkEditSubmitActionInitializer', schema: { 'x-action-settings': {}, }, @@ -388,16 +304,18 @@ export const BulkEditFormActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { type: 'subMenu', title: '{{t("Customize")}}', + name: 'customize', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -445,9 +363,9 @@ export const BulkEditFormActionInitializers = { }, }, { - type: 'item', + name: 'saveRecord', title: '{{t("Save record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Save") }}', 'x-component': 'Action', @@ -472,11 +390,11 @@ export const BulkEditFormActionInitializers = { }, }, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', }, ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx index c25dcadb37..089f019727 100644 --- a/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/FormItemInitializers.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerChildren } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCompile } from '../../schema-component'; -import { SchemaInitializer } from '../SchemaInitializer'; import { gridRowColWrap, useAssociatedFormItemInitializerFields, @@ -12,131 +13,70 @@ import { useInheritsFormItemInitializerFields, } from '../utils'; -// 表单里配置字段 -export const FormItemInitializers = (props: any) => { +const ParentCollectionFields = () => { + const inheritFields = useInheritsFormItemInitializerFields(); const { t } = useTranslation(); - const { insertPosition, component } = props; + const compile = useCompile(); + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + return {res}; +}; + +const AssociatedFields = () => { const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form', }); - const inheritFields = useInheritsFormItemInitializerFields(); - const compile = useCompile(); - const fieldItems: any[] = [ - { - type: 'itemGroup', - title: t('Display fields'), - children: useFormItemInitializerFields(), - }, - ]; - if (inheritFields?.length > 0) { - inheritFields.forEach((inherit) => { - Object.values(inherit)[0].length && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0], - }, - ); - }); - } - associationFields.length > 0 && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t('Display association fields'), - children: associationFields, - }, - ); - - fieldItems.push( - { - type: 'divider', - }, - { - type: 'item', - title: t('Add text'), - component: 'BlockInitializer', - schema: { - type: 'void', - 'x-editable': false, - 'x-decorator': 'FormItem', - 'x-designer': 'Markdown.Void.Designer', - 'x-component': 'Markdown.Void', - 'x-component-props': { - content: t('This is a demo text, **supports Markdown syntax**.'), - }, - }, - }, - ); - return ( - - ); -}; - -export const FilterFormItemInitializers = (props: any) => { const { t } = useTranslation(); - const { insertPosition, component } = props; - const associationFields = useFilterAssociatedFormItemInitializerFields(); - const inheritFields = useFilterInheritsFormItemInitializerFields(); - const compile = useCompile(); - const fieldItems: any[] = [ + if (associationFields.length === 0) return null; + const schema: any = [ { type: 'itemGroup', - title: t('Display fields'), - children: useFilterFormItemInitializerFields(), + title: t('Display association fields'), + children: associationFields, }, ]; - if (inheritFields?.length > 0) { - inheritFields.forEach((inherit) => { - Object.values(inherit)[0].length && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0], - }, - ); - }); - } + return {schema}; +}; - associationFields.length > 0 && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t('Display association fields'), - children: associationFields, - }, - ); - - fieldItems.push( +// 表单里配置字段 +export const formItemInitializers = new SchemaInitializer({ + name: 'FormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ { + type: 'itemGroup', + name: 'displayFields', + title: '{{t("Display fields")}}', + useChildren: useFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'divider', type: 'divider', }, { - type: 'item', - title: t('Add text'), - component: 'BlockInitializer', + name: 'addText', + title: '{{t("Add text")}}', + Component: 'BlockItemInitializer', schema: { type: 'void', 'x-editable': false, @@ -144,19 +84,84 @@ export const FilterFormItemInitializers = (props: any) => { 'x-designer': 'Markdown.Void.Designer', 'x-component': 'Markdown.Void', 'x-component-props': { - content: t('This is a demo text, **supports Markdown syntax**.'), + content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', }, }, }, - ); - return ( - - ); + ], +}); + +export const FilterParentCollectionFields = () => { + const inheritFields = useFilterInheritsFormItemInitializerFields(); + const { t } = useTranslation(); + const compile = useCompile(); + const res = []; + if (inheritFields?.length > 0) { + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + divider: true, + type: 'itemGroup', + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + } + + return {res}; }; + +export const FilterAssociatedFields = () => { + const associationFields = useFilterAssociatedFormItemInitializerFields(); + const { t } = useTranslation(); + const res: any[] = [ + { + type: 'itemGroup', + title: t('Display association fields'), + children: associationFields, + }, + ]; + return {res}; +}; + +export const filterFormItemInitializers = new SchemaInitializer({ + name: 'FilterFormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + name: 'displayFields', + title: '{{t("Display fields")}}', + useChildren: useFilterFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: FilterParentCollectionFields, + }, + { + name: 'associationFields', + Component: FilterAssociatedFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + title: '{{t("Add text")}}', + Component: 'BlockItemInitializer', + name: 'addText', + schema: { + type: 'void', + 'x-editable': false, + 'x-decorator': 'FormItem', + 'x-designer': 'Markdown.Void.Designer', + 'x-component': 'Markdown.Void', + 'x-component-props': { + content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', + }, + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/GanttActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/GanttActionInitializers.tsx index 8305016da8..56a39610fe 100644 --- a/packages/core/client/src/schema-initializer/buttons/GanttActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/GanttActionInitializers.tsx @@ -1,8 +1,163 @@ -import { TableActionInitializers } from './TableActionInitializers'; -// 甘特图区块action配置 -export const GanttActionInitializers = { - ...TableActionInitializers, - // items: TableActionInitializers.items.filter((v) => { - // return v.component !== 'ActionBarAssociationFilterAction'; - // }), -}; +import { useFieldSchema } from '@formily/react'; +import { useCollection } from '../../'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; + +export const ganttActionInitializers = new SchemaInitializer({ + name: 'GanttActionInitializers', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + name: 'enableActions', + title: "{{t('Enable actions')}}", + children: [ + { + type: 'item', + name: 'filter', + title: "{{t('Filter')}}", + Component: 'FilterActionInitializer', + schema: { + 'x-align': 'left', + }, + }, + { + type: 'item', + title: "{{t('Add new')}}", + name: 'addNew', + Component: 'CreateActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + useVisible() { + const collection = useCollection(); + return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; + }, + }, + { + type: 'item', + title: "{{t('Delete')}}", + name: 'delete', + Component: 'BulkDestroyActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + }, + useVisible() { + const collection = useCollection(); + return !['view', 'sql'].includes(collection.template) || collection?.writableView; + }, + }, + { + type: 'item', + title: "{{t('Refresh')}}", + name: 'refresh', + Component: 'RefreshActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + { + name: 'toggle', + title: "{{t('Expand/Collapse')}}", + Component: 'ExpandActionInitializer', + schema: { + 'x-align': 'right', + }, + useVisible() { + const schema = useFieldSchema(); + const collection = useCollection(); + const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; + return collection.tree && treeTable !== false; + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + useVisible() { + const collection = useCollection(); + return !['view', 'sql'].includes(collection.template) || collection?.writableView; + }, + }, + { + type: 'subMenu', + name: 'customize', + title: '{{t("Customize")}}', + children: [ + { + type: 'item', + title: '{{t("Bulk update")}}', + Component: 'CustomizeActionInitializer', + name: 'bulkUpdate', + schema: { + type: 'void', + title: '{{ t("Bulk update") }}', + 'x-component': 'Action', + 'x-align': 'right', + 'x-acl-action': 'update', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-action': 'customize:bulkUpdate', + 'x-designer': 'Action.Designer', + 'x-action-settings': { + assignedValues: {}, + updateMode: 'selected', + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: '{{t("Updated successfully")}}', + }, + }, + 'x-component-props': { + icon: 'EditOutlined', + useProps: '{{ useCustomizeBulkUpdateActionProps }}', + }, + }, + }, + { + type: 'item', + title: '{{t("Bulk edit")}}', + name: 'bulkEdit', + Component: 'CustomizeBulkEditActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'update', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + }, + { + type: 'item', + title: '{{t("Add record")}}', + name: 'addRecord', + Component: 'CustomizeAddRecordActionInitializer', + schema: { + 'x-align': 'right', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'create', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + }, + }, + ], + useVisible() { + const collection = useCollection(); + return !['view', 'sql'].includes(collection.template) || collection?.writableView; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx index 955d24f643..a9f66c3209 100644 --- a/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/GridCardActionInitializers.tsx @@ -1,7 +1,9 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCollection } from '../../collection-manager'; // 表单的操作配置 -export const GridCardActionInitializers = { +export const gridCardActionInitializers = new SchemaInitializer({ + name: 'GridCardActionInitializers', title: "{{t('Configure actions')}}", icon: 'SettingOutlined', style: { @@ -11,19 +13,20 @@ export const GridCardActionInitializers = { { type: 'itemGroup', title: "{{t('Enable actions')}}", + name: 'enableActions', children: [ { - type: 'item', + name: 'filter', title: "{{t('Filter')}}", - component: 'FilterActionInitializer', + Component: 'FilterActionInitializer', schema: { 'x-align': 'left', }, }, { - type: 'item', + name: 'addNew', title: "{{t('Add new')}}", - component: 'CreateActionInitializer', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -31,23 +34,23 @@ export const GridCardActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; }, }, { - type: 'item', + name: 'refresh', title: "{{t('Refresh')}}", - component: 'RefreshActionInitializer', + Component: 'RefreshActionInitializer', schema: { 'x-align': 'right', }, }, { - type: 'item', + name: 'import', title: "{{t('Import')}}", - component: 'ImportActionInitializer', + Component: 'ImportActionInitializer', schema: { 'x-align': 'right', 'x-acl-action': 'importXlsx', @@ -56,15 +59,15 @@ export const GridCardActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'export', title: "{{t('Export')}}", - component: 'ExportActionInitializer', + Component: 'ExportActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -75,82 +78,23 @@ export const GridCardActionInitializers = { }, ], }, - // { - // type: 'divider', - // visible: () => { - // const collection = useCollection(); - // return (collection as any).template !== 'view'; - // }, - // }, - // { - // type: 'subMenu', - // title: '{{t("Customize")}}', - // children: [ - // { - // type: 'item', - // title: '{{t("Bulk update")}}', - // component: 'CustomizeActionInitializer', - // schema: { - // type: 'void', - // title: '{{ t("Bulk update") }}', - // 'x-component': 'Action', - // 'x-align': 'right', - // 'x-acl-action': 'update', - // 'x-decorator': 'ACLActionProvider', - // 'x-acl-action-props': { - // skipScopeCheck: true, - // }, - // 'x-action': 'customize:bulkUpdate', - // 'x-designer': 'Action.Designer', - // 'x-action-settings': { - // assignedValues: {}, - // updateMode: 'selected', - // onSuccess: { - // manualClose: true, - // redirecting: false, - // successMessage: '{{t("Updated successfully")}}', - // }, - // }, - // 'x-component-props': { - // icon: 'EditOutlined', - // useProps: '{{ useCustomizeBulkUpdateActionProps }}', - // }, - // }, - // }, - // { - // type: 'item', - // title: '{{t("Bulk edit")}}', - // component: 'CustomizeBulkEditActionInitializer', - // schema: { - // 'x-align': 'right', - // 'x-decorator': 'ACLActionProvider', - // 'x-acl-action': 'update', - // 'x-acl-action-props': { - // skipScopeCheck: true, - // }, - // }, - // }, - // ], - // visible: () => { - // const collection = useCollection(); - // return (collection as any).template !== 'view'; - // }, - // }, ], -}; +}); -export const GridCardItemActionInitializers = { +export const gridCardItemActionInitializers = new SchemaInitializer({ + name: 'GridCardItemActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', title: '{{t("Enable actions")}}', + name: 'enable-actions', children: [ { - type: 'item', + name: 'view', title: '{{t("View")}}', - component: 'ViewActionInitializer', + Component: 'ViewActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'view', @@ -159,31 +103,31 @@ export const GridCardItemActionInitializers = { }, }, { - type: 'item', + name: 'edit', title: '{{t("Edit")}}', - component: 'UpdateActionInitializer', + Component: 'UpdateActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'update', 'x-decorator': 'ACLActionProvider', 'x-align': 'left', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'delete', title: '{{t("Delete")}}', - component: 'DestroyActionInitializer', + Component: 'DestroyActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'destroy', 'x-decorator': 'ACLActionProvider', 'x-align': 'left', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return collection.template !== 'sql'; }, @@ -191,16 +135,18 @@ export const GridCardItemActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { type: 'subMenu', title: '{{t("Customize")}}', + name: 'customize', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -248,9 +194,9 @@ export const GridCardItemActionInitializers = { }, }, { - type: 'item', + name: 'update-record', title: '{{t("Update record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Update record") }}', 'x-component': 'Action.Link', @@ -270,19 +216,19 @@ export const GridCardItemActionInitializers = { useProps: '{{ useCustomizeUpdateActionProps }}', }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'custom-request', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', schema: { 'x-action': 'customize:table:request', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, @@ -290,4 +236,4 @@ export const GridCardItemActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/KanbanActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/KanbanActionInitializers.tsx index 1ef7e77d2d..c67859ed76 100644 --- a/packages/core/client/src/schema-initializer/buttons/KanbanActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/KanbanActionInitializers.tsx @@ -1,6 +1,8 @@ import { useCollection } from '../../'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; -export const KanbanActionInitializers = { +export const kanbanActionInitializers = new SchemaInitializer({ + name: 'KanbanActionInitializers', title: "{{t('Configure actions')}}", icon: 'SettingOutlined', style: { @@ -10,19 +12,20 @@ export const KanbanActionInitializers = { { type: 'itemGroup', title: "{{t('Enable actions')}}", + name: 'enableActions', children: [ { - type: 'item', + name: 'filter', title: "{{t('Filter')}}", - component: 'FilterActionInitializer', + Component: 'FilterActionInitializer', schema: { 'x-align': 'left', }, }, { - type: 'item', + name: 'addNew', title: "{{t('Add new')}}", - component: 'CreateActionInitializer', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -30,7 +33,7 @@ export const KanbanActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection as any).template !== 'view' || collection?.writableView; }, @@ -38,4 +41,4 @@ export const KanbanActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/KanbanCardFormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/KanbanCardFormItemInitializers.tsx deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx index 24d3071dc3..70219da826 100644 --- a/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/ListActionInitializers.tsx @@ -1,7 +1,9 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCollection } from '../../collection-manager'; // 表单的操作配置 -export const ListActionInitializers = { +export const listActionInitializers = new SchemaInitializer({ + name: 'ListActionInitializers', title: "{{t('Configure actions')}}", icon: 'SettingOutlined', style: { @@ -10,20 +12,21 @@ export const ListActionInitializers = { items: [ { type: 'itemGroup', + name: 'enableActions', title: "{{t('Enable actions')}}", children: [ { - type: 'item', + name: 'filter', title: "{{t('Filter')}}", - component: 'FilterActionInitializer', + Component: 'FilterActionInitializer', schema: { 'x-align': 'left', }, }, { - type: 'item', + name: 'addNew', title: "{{t('Add new')}}", - component: 'CreateActionInitializer', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -31,7 +34,7 @@ export const ListActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return ( (collection.template !== 'view' || collection?.writableView) && @@ -41,17 +44,17 @@ export const ListActionInitializers = { }, }, { - type: 'item', + name: 'refresh', title: "{{t('Refresh')}}", - component: 'RefreshActionInitializer', + Component: 'RefreshActionInitializer', schema: { 'x-align': 'right', }, }, { - type: 'item', + name: 'import', title: "{{t('Import')}}", - component: 'ImportActionInitializer', + Component: 'ImportActionInitializer', schema: { 'x-align': 'right', 'x-acl-action': 'importXlsx', @@ -60,15 +63,15 @@ export const ListActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'export', title: "{{t('Export')}}", - component: 'ExportActionInitializer', + Component: 'ExportActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -79,82 +82,23 @@ export const ListActionInitializers = { }, ], }, - // { - // type: 'divider', - // visible: () => { - // const collection = useCollection(); - // return (collection as any).template !== 'view'; - // }, - // }, - // { - // type: 'subMenu', - // title: '{{t("Customize")}}', - // children: [ - // { - // type: 'item', - // title: '{{t("Bulk update")}}', - // component: 'CustomizeActionInitializer', - // schema: { - // type: 'void', - // title: '{{ t("Bulk update") }}', - // 'x-component': 'Action', - // 'x-align': 'right', - // 'x-acl-action': 'update', - // 'x-decorator': 'ACLActionProvider', - // 'x-acl-action-props': { - // skipScopeCheck: true, - // }, - // 'x-action': 'customize:bulkUpdate', - // 'x-designer': 'Action.Designer', - // 'x-action-settings': { - // assignedValues: {}, - // updateMode: 'selected', - // onSuccess: { - // manualClose: true, - // redirecting: false, - // successMessage: '{{t("Updated successfully")}}', - // }, - // }, - // 'x-component-props': { - // icon: 'EditOutlined', - // useProps: '{{ useCustomizeBulkUpdateActionProps }}', - // }, - // }, - // }, - // { - // type: 'item', - // title: '{{t("Bulk edit")}}', - // component: 'CustomizeBulkEditActionInitializer', - // schema: { - // 'x-align': 'right', - // 'x-decorator': 'ACLActionProvider', - // 'x-acl-action': 'update', - // 'x-acl-action-props': { - // skipScopeCheck: true, - // }, - // }, - // }, - // ], - // visible: () => { - // const collection = useCollection(); - // return (collection as any).template !== 'view'; - // }, - // }, ], -}; +}); -export const ListItemActionInitializers = { +export const listItemActionInitializers = new SchemaInitializer({ + name: 'ListItemActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', items: [ { type: 'itemGroup', + name: 'enableActions', title: '{{t("Enable actions")}}', children: [ { - type: 'item', + name: 'view', title: '{{t("View")}}', - component: 'ViewActionInitializer', + Component: 'ViewActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'view', @@ -163,31 +107,31 @@ export const ListItemActionInitializers = { }, }, { - type: 'item', + name: 'edit', title: '{{t("Edit")}}', - component: 'UpdateActionInitializer', + Component: 'UpdateActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'update', 'x-decorator': 'ACLActionProvider', 'x-align': 'left', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'delete', title: '{{t("Delete")}}', - component: 'DestroyActionInitializer', + Component: 'DestroyActionInitializer', schema: { 'x-component': 'Action.Link', 'x-action': 'destroy', 'x-decorator': 'ACLActionProvider', 'x-align': 'left', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return collection.template !== 'sql'; }, @@ -195,16 +139,18 @@ export const ListItemActionInitializers = { ], }, { + name: 'divider', type: 'divider', }, { type: 'subMenu', title: '{{t("Customize")}}', + name: 'customize', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -252,9 +198,9 @@ export const ListItemActionInitializers = { }, }, { - type: 'item', + name: 'updateRecord', title: '{{t("Update record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Update record") }}', 'x-component': 'Action.Link', @@ -274,19 +220,19 @@ export const ListItemActionInitializers = { useProps: '{{ useCustomizeUpdateActionProps }}', }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, }, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', + Component: 'CustomRequestInitializer', schema: { 'x-action': 'customize:table:request', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }, @@ -294,4 +240,4 @@ export const ListItemActionInitializers = { ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx index 116da49dc5..c73bbe4ffa 100644 --- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormActionInitializers.tsx @@ -1,11 +1,13 @@ import { useCollection } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; const useVisibleCollection = () => { const collection = useCollection(); return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; }; // 表单的操作配置 -export const ReadPrettyFormActionInitializers = { +export const readPrettyFormActionInitializers = new SchemaInitializer({ + name: 'ReadPrettyFormActionInitializers', title: '{{t("Configure actions")}}', icon: 'SettingOutlined', style: { @@ -14,12 +16,13 @@ export const ReadPrettyFormActionInitializers = { items: [ { type: 'itemGroup', + name: 'enableActions', title: '{{t("Enable actions")}}', children: [ { - type: 'item', title: '{{t("Edit")}}', - component: 'UpdateActionInitializer', + name: 'edit', + Component: 'UpdateActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', @@ -27,22 +30,22 @@ export const ReadPrettyFormActionInitializers = { type: 'primary', }, }, - visible: useVisibleCollection, + useVisible: useVisibleCollection, }, { - type: 'item', title: '{{t("Delete")}}', - component: 'DestroyActionInitializer', + name: 'delete', + Component: 'DestroyActionInitializer', schema: { 'x-component': 'Action', 'x-decorator': 'ACLActionProvider', }, - visible: useVisibleCollection, + useVisible: useVisibleCollection, }, { - type: 'item', title: '{{t("Duplicate")}}', - component: 'DuplicateActionInitializer', + name: 'duplicate', + Component: 'DuplicateActionInitializer', schema: { 'x-component': 'Action', 'x-action': 'duplicate', @@ -51,30 +54,31 @@ export const ReadPrettyFormActionInitializers = { type: 'primary', }, }, - visible: useVisibleCollection, + useVisible: useVisibleCollection, }, { - type: 'item', title: '{{t("Print")}}', - component: 'PrintActionInitializer', + name: 'print', + Component: 'PrintActionInitializer', schema: { 'x-component': 'Action', }, }, ], }, - { + name: 'divider', type: 'divider', }, { type: 'subMenu', + name: 'customize', title: '{{t("Customize")}}', children: [ { - type: 'item', + name: 'popup', title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { type: 'void', title: '{{ t("Popup") }}', @@ -122,9 +126,9 @@ export const ReadPrettyFormActionInitializers = { }, }, { - type: 'item', + name: 'updateRecord', title: '{{t("Update record")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', schema: { title: '{{ t("Update record") }}', 'x-component': 'Action', @@ -147,12 +151,12 @@ export const ReadPrettyFormActionInitializers = { visible: useVisibleCollection, }, { - type: 'item', + name: 'customRequest', title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', - visible: useVisibleCollection, + Component: 'CustomRequestInitializer', + useVisible: useVisibleCollection, }, ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx index 403a03e68f..357817dd74 100644 --- a/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/ReadPrettyFormItemInitializers.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerChildren } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCompile } from '../../schema-component'; -import { SchemaInitializer } from '../SchemaInitializer'; import { gridRowColWrap, useAssociatedFormItemInitializerFields, @@ -9,54 +10,69 @@ import { useInheritsFormItemInitializerFields, } from '../utils'; -export const ReadPrettyFormItemInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component } = props; - const associationFields = useAssociatedFormItemInitializerFields({ readPretty: true, block: 'Form' }); +const ParentCollectionFields = () => { const inheritFields = useInheritsFormItemInitializerFields(); + const { t } = useTranslation(); const compile = useCompile(); - const fieldItems: any[] = [ + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + return {res}; +}; + +const AssociatedFields = () => { + const associationFields = useAssociatedFormItemInitializerFields({ + readPretty: true, + block: 'Form', + }); + const { t } = useTranslation(); + if (associationFields.length === 0) return null; + const schema: any = [ { type: 'itemGroup', - title: t('Display fields'), - children: useFormItemInitializerFields(), + title: t('Display association fields'), + children: associationFields, }, ]; - if (inheritFields?.length > 0) { - inheritFields.forEach((inherit) => { - Object.values(inherit)[0]?.length && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0], - }, - ); - }); - } - associationFields.length > 0 && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t('Display association fields'), - children: associationFields, - }, - ); + return {schema}; +}; - fieldItems.push( +export const readPrettyFormItemInitializers = new SchemaInitializer({ + name: 'ReadPrettyFormItemInitializers', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ { + type: 'itemGroup', + name: 'displayFields', + title: '{{t("Display fields")}}', + useChildren: useFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'divider', type: 'divider', }, { - type: 'item', - title: t('Add text'), - component: 'BlockInitializer', + name: 'addText', + title: '{{t("Add text")}}', + Component: 'BlockItemInitializer', schema: { type: 'void', 'x-editable': false, @@ -64,19 +80,9 @@ export const ReadPrettyFormItemInitializers = (props: any) => { 'x-designer': 'Markdown.Void.Designer', 'x-component': 'Markdown.Void', 'x-component-props': { - content: t('This is a demo text, **supports Markdown syntax**.'), + content: '{{t("This is a demo text, **supports Markdown syntax**.")}}', }, }, }, - ); - return ( - - ); -}; + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index 943a149cb8..4cbdc739d1 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -1,7 +1,7 @@ import { Schema, useFieldSchema } from '@formily/react'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer, SchemaInitializerItemOptions, useCollection, useCollectionManager } from '../..'; +import { useCollection, useCollectionManager } from '../..'; +import { SchemaInitializerItemType, useSchemaInitializer } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; const recursiveParent = (schema: Schema) => { @@ -43,7 +43,7 @@ const useRelationFields = () => { type: 'item', title: '{{t("Details")}}', field, - component: 'RecordReadPrettyAssociationFormBlockInitializer', + Component: 'RecordReadPrettyAssociationFormBlockInitializer', }, // { // key: `${field.name}_form`, @@ -67,42 +67,42 @@ const useRelationFields = () => { type: 'item', title: '{{t("Table")}}', field, - component: 'RecordAssociationBlockInitializer', + Component: 'RecordAssociationBlockInitializer', }, { key: `${field.name}_details`, type: 'item', title: '{{t("Details")}}', field, - component: 'RecordAssociationDetailsBlockInitializer', + Component: 'RecordAssociationDetailsBlockInitializer', }, { key: `${field.name}_list`, type: 'item', title: '{{t("List")}}', field, - component: 'RecordAssociationListBlockInitializer', + Component: 'RecordAssociationListBlockInitializer', }, { key: `${field.name}_grid_card`, type: 'item', title: '{{t("Grid Card")}}', field, - component: 'RecordAssociationGridCardBlockInitializer', + Component: 'RecordAssociationGridCardBlockInitializer', }, { key: `${field.name}_form`, type: 'item', title: '{{t("Form")}}', field, - component: 'RecordAssociationFormBlockInitializer', + Component: 'RecordAssociationFormBlockInitializer', }, { key: `${field.name}_calendar`, type: 'item', title: '{{t("Calendar")}}', field, - component: 'RecordAssociationCalendarBlockInitializer', + Component: 'RecordAssociationCalendarBlockInitializer', }, ], }; @@ -113,9 +113,10 @@ const useRelationFields = () => { type: 'item', field, title: field?.uiSchema?.title || field.name, - component: 'RecordAssociationBlockInitializer', + Component: 'RecordAssociationBlockInitializer', }; }) as any; + return relationFields; }; @@ -123,10 +124,11 @@ const useDetailCollections = (props) => { const { actionInitializers, childrenCollections, collection } = props; const detailCollections = [ { + name: collection.name, key: collection.name, type: 'item', title: collection?.title || collection.name, - component: 'RecordReadPrettyFormBlockInitializer', + Component: 'RecordReadPrettyFormBlockInitializer', icon: false, targetCollection: collection, actionInitializers, @@ -134,16 +136,17 @@ const useDetailCollections = (props) => { ].concat( childrenCollections.map((c) => { return { + name: c.name, key: c.name, type: 'item', title: c?.title || c.name, - component: 'RecordReadPrettyFormBlockInitializer', + Component: 'RecordReadPrettyFormBlockInitializer', icon: false, targetCollection: c, actionInitializers, }; }), - ) as SchemaInitializerItemOptions[]; + ) as SchemaInitializerItemType[]; return detailCollections; }; @@ -151,10 +154,11 @@ const useFormCollections = (props) => { const { actionInitializers, childrenCollections, collection } = props; const formCollections = [ { + name: collection.name, key: collection.name, type: 'item', title: collection?.title || collection.name, - component: 'RecordFormBlockInitializer', + Component: 'RecordFormBlockInitializer', icon: false, targetCollection: collection, actionInitializers, @@ -162,23 +166,24 @@ const useFormCollections = (props) => { ].concat( childrenCollections.map((c) => { return { + name: c.name, key: c.name, type: 'item', title: c?.title || c.name, - component: 'RecordFormBlockInitializer', + Component: 'RecordFormBlockInitializer', icon: false, targetCollection: c, actionInitializers, }; }), - ) as SchemaInitializerItemOptions[]; + ) as SchemaInitializerItemType[]; return formCollections; }; -export const RecordBlockInitializers = (props: any) => { - const { t } = useTranslation(); - const { insertPosition, component, actionInitializers } = props; +function useRecordBlocks() { + const { options } = useSchemaInitializer(); + const { actionInitializers } = options; const collection = useCollection(); const { getChildrenCollections } = useCollectionManager(); const formChildrenCollections = getChildrenCollections(collection.name); @@ -187,81 +192,85 @@ export const RecordBlockInitializers = (props: any) => { const hasDetailChildCollection = detailChildrenCollections?.length > 0; const modifyFlag = (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; const detailChildren = useDetailCollections({ - ...props, + ...options, childrenCollections: detailChildrenCollections, collection, }); const formChildren = useFormCollections({ - ...props, + ...options, childrenCollections: formChildrenCollections, collection, }); - return ( - 0; + }, + }, + { + type: 'itemGroup', + name: 'otherBlocks', + title: '{{t("Other blocks")}}', + children: [ { - type: 'itemGroup', - title: '{{t("Current record blocks")}}', - children: [ - hasDetailChildCollection - ? { - key: 'details', - type: 'subMenu', - title: '{{t("Details")}}', - children: detailChildren, - } - : { - key: 'details', - type: 'item', - title: '{{t("Details")}}', - component: 'RecordReadPrettyFormBlockInitializer', - actionInitializers, - }, - hasFormChildCollection - ? { - key: 'form', - type: 'subMenu', - title: '{{t("Form")}}', - children: formChildren, - } - : modifyFlag && { - key: 'form', - type: 'item', - title: '{{t("Form")}}', - component: 'RecordFormBlockInitializer', - }, - ], + name: 'markdown', + title: '{{t("Markdown")}}', + Component: 'MarkdownBlockInitializer', }, - { - type: 'itemGroup', - title: '{{t("Relationship blocks")}}', - children: useRelationFields(), - }, - { - type: 'itemGroup', - title: '{{t("Other blocks")}}', - children: [ - { - key: 'markdown', - type: 'item', - title: '{{t("Markdown")}}', - component: 'MarkdownBlockInitializer', - }, - { - key: 'auditLogs', - type: 'item', - title: '{{t("Audit logs")}}', - component: 'AuditLogsBlockInitializer', - }, - ], - }, - ]} - /> - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx index 8abcae9002..06ef0c4d93 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordFormBlockInitializers.tsx @@ -1,39 +1,35 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; -export const RecordFormBlockInitializers = (props: any) => { - const { t } = useTranslation(); - return ( - - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/SubTableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/SubTableActionInitializers.tsx index 86e72c2c67..7068482e7f 100644 --- a/packages/core/client/src/schema-initializer/buttons/SubTableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/SubTableActionInitializers.tsx @@ -1,39 +1,36 @@ +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; + // 表格操作配置 -export const SubTableActionInitializers = { +export const subTableActionInitializers = new SchemaInitializer({ + name: 'SubTableActionInitializers', title: "{{t('Configure actions')}}", icon: 'SettingOutlined', style: { marginLeft: 8, }, - // size: 'small', items: [ { type: 'itemGroup', title: "{{t('Enable actions')}}", + name: 'enableActions', children: [ { - type: 'item', + name: 'addNew', title: "{{t('Add new')}}", - component: 'CreateActionInitializer', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', - // 'x-component-props': { - // size: 'small', - // }, }, }, { - type: 'item', + name: 'delete', title: "{{t('Delete')}}", - component: 'BulkDestroyActionInitializer', + Component: 'BulkDestroyActionInitializer', schema: { 'x-align': 'right', - // 'x-component-props': { - // size: 'small', - // }, }, }, ], }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx index d18bbabd39..8427550c4b 100644 --- a/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TabPaneInitializers.tsx @@ -1,22 +1,25 @@ import { useForm } from '@formily/react'; import React, { useMemo } from 'react'; import { SchemaComponent, useActionContext, useDesignable, useRecordIndex } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useGetAriaLabelOfSchemaInitializer } from '../hooks/useGetAriaLabelOfSchemaInitializer'; export const TabPaneInitializers = (props?: any) => { const { designable, insertBeforeEnd } = useDesignable(); + const { isCreate, isBulkEdit, options } = props; + const { gridInitializer } = options; const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); const useSubmitAction = () => { const form = useForm(); const ctx = useActionContext(); const index = useRecordIndex(); - let initializer = props.gridInitializer; + let initializer = gridInitializer; if (!initializer) { initializer = 'RecordBlockInitializers'; - if (props.isCreate || index === null) { + if (isCreate || index === null) { initializer = 'CreateFormBlockInitializers'; - } else if (props.isBulkEdit) { + } else if (isBulkEdit) { initializer = 'CreateFormBulkEditBlockInitializers'; } } @@ -134,3 +137,21 @@ export const TabPaneInitializersForCreateFormBlock = (props) => { export const TabPaneInitializersForBulkEditFormBlock = (props) => { return ; }; + +export const tabPaneInitializers = new SchemaInitializer({ + name: 'TabPaneInitializers', + Component: TabPaneInitializers, + popover: false, +}); + +export const tabPaneInitializersForRecordBlock = new SchemaInitializer({ + name: 'TabPaneInitializersForCreateFormBlock', + Component: TabPaneInitializersForCreateFormBlock, + popover: false, +}); + +export const tabPaneInitializersForBulkEditFormBlock = new SchemaInitializer({ + name: 'TabPaneInitializersForBulkEditFormBlock', + Component: TabPaneInitializersForBulkEditFormBlock, + popover: false, +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx index 57702b5c14..aa3c3dce18 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx @@ -3,19 +3,35 @@ import { ISchema, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializer, SchemaSettings } from '../..'; import { useAPIClient } from '../../api-client'; +import { SchemaInitializerActionModal, SchemaInitializerItem, useSchemaInitializer } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCollection } from '../../collection-manager'; import { createDesignable, useDesignable } from '../../schema-component'; import { useGetAriaLabelOfDesigner } from '../../schema-settings/hooks/useGetAriaLabelOfDesigner'; -export const Resizable = (props) => { +export const Resizable = () => { const { t } = useTranslation(); const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); return ( - ((props, ref) => { + const { children, onClick, ...others } = props; + const { setVisible } = useSchemaInitializer(); + return ( + { + setVisible(false); + onClick(event); + }} + {...others} + title={t('Column width')} + > + ); + })} schema={ { type: 'object', @@ -47,154 +63,178 @@ export const Resizable = (props) => { ); }; -export const TableActionColumnInitializers = (props: any) => { - const fieldSchema = useFieldSchema(); - const api = useAPIClient(); - const { refresh } = useDesignable(); - const { t } = useTranslation(); - const collection = useCollection(); - const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {}; - const { getAriaLabel } = useGetAriaLabelOfDesigner(); - return ( - { - const spaceSchema = fieldSchema.reduceProperties((buf, schema) => { - if (schema['x-component'] === 'Space') { - return schema; - } - return buf; - }, null); - if (!spaceSchema) { - return; - } - _.set(schema, 'x-designer-props.linkageAction', true); - const dn = createDesignable({ - t, - api, - refresh, - current: spaceSchema, - }); - dn.loadAPIClientEvents(); - dn.insertBeforeEnd(schema); - }} - items={[ - { - type: 'itemGroup', - title: t('Enable actions'), - children: [ - { - type: 'item', - title: t('View'), - component: 'ViewActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'view', - 'x-decorator': 'ACLActionProvider', - }, - }, - { - type: 'item', - title: t('Edit'), - component: 'UpdateActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'update', - 'x-decorator': 'ACLActionProvider', - }, - visible: () => { - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, +export const tableActionColumnInitializers = new SchemaInitializer({ + name: 'TableActionColumnInitializers', + insertPosition: 'beforeEnd', + useInsert: function useInsert() { + const { refresh } = useDesignable(); + const fieldSchema = useFieldSchema(); + const api = useAPIClient(); + const { t } = useTranslation(); - { - type: 'item', - title: t('Delete'), - component: 'DestroyActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'destroy', - 'x-decorator': 'ACLActionProvider', - }, - visible: () => { - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - collection.tree && - treeTable !== false && { - type: 'item', - title: t('Add child'), - component: 'CreateChildInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'create', - 'x-decorator': 'ACLActionProvider', - }, - }, - { - type: 'item', - title: t('Duplicate'), - component: 'DuplicateActionInitializer', - schema: { - 'x-component': 'Action.Link', - 'x-action': 'duplicate', - 'x-decorator': 'ACLActionProvider', - }, - visible: () => { - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - ], + return function insert(schema) { + const spaceSchema = fieldSchema.reduceProperties((buf, schema) => { + if (schema['x-component'] === 'Space') { + return schema; + } + return buf; + }, null); + if (!spaceSchema) { + return; + } + _.set(schema, 'x-designer-props.linkageAction', true); + const dn = createDesignable({ + t, + api, + refresh, + current: spaceSchema, + }); + dn.loadAPIClientEvents(); + dn.insertBeforeEnd(schema); + }; + }, + Component: (props: any) => { + const { getAriaLabel } = useGetAriaLabelOfDesigner(); + return ( + + ); + }, + items: [ + { + type: 'itemGroup', + name: 'actions', + title: '{{t("Enable actions")}}', + children: [ + { + type: 'item', + title: '{{t("View")}}', + name: 'view', + Component: 'ViewActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'view', + 'x-decorator': 'ACLActionProvider', + }, }, { - type: 'divider', + type: 'item', + name: 'edit', + title: '{{t("Edit")}}', + Component: 'UpdateActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'update', + 'x-decorator': 'ACLActionProvider', + }, + useVisible() { + const collection = useCollection(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, }, { - type: 'subMenu', - title: '{{t("Customize")}}', - children: [ - { - type: 'item', - title: '{{t("Popup")}}', - component: 'CustomizeActionInitializer', - schema: { + type: 'item', + title: '{{t("Delete")}}', + name: 'delete', + Component: 'DestroyActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'destroy', + 'x-decorator': 'ACLActionProvider', + }, + useVisible() { + const collection = useCollection(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + type: 'item', + title: '{{t("Add child")}}', + name: 'addChildren', + Component: 'CreateChildInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'create', + 'x-decorator': 'ACLActionProvider', + }, + useVisible() { + const fieldSchema = useFieldSchema(); + const collection = useCollection(); + const { treeTable } = fieldSchema?.parent?.parent['x-decorator-props'] || {}; + return collection.tree && treeTable !== false; + }, + }, + { + type: 'item', + title: '{{t("Duplicate")}}', + name: 'duplicate', + Component: 'DuplicateActionInitializer', + schema: { + 'x-component': 'Action.Link', + 'x-action': 'duplicate', + 'x-decorator': 'ACLActionProvider', + }, + useVisible() { + const collection = useCollection(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'subMenu', + title: '{{t("Customize")}}', + name: 'customize', + children: [ + { + type: 'item', + title: '{{t("Popup")}}', + name: 'popup', + Component: 'CustomizeActionInitializer', + schema: { + type: 'void', + title: '{{ t("Popup") }}', + 'x-action': 'customize:popup', + 'x-designer': 'Action.Designer', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + }, + properties: { + drawer: { type: 'void', title: '{{ t("Popup") }}', - 'x-action': 'customize:popup', - 'x-designer': 'Action.Designer', - 'x-component': 'Action.Link', + 'x-component': 'Action.Container', 'x-component-props': { - openMode: 'drawer', + className: 'nb-action-popup', }, properties: { - drawer: { + tabs: { type: 'void', - title: '{{ t("Popup") }}', - 'x-component': 'Action.Container', - 'x-component-props': { - className: 'nb-action-popup', - }, + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'TabPaneInitializers', properties: { - tabs: { + tab1: { type: 'void', - 'x-component': 'Tabs', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', 'x-component-props': {}, - 'x-initializer': 'TabPaneInitializers', properties: { - tab1: { + grid: { type: 'void', - title: '{{t("Details")}}', - 'x-component': 'Tabs.TabPane', - 'x-designer': 'Tabs.Designer', - 'x-component-props': {}, - properties: { - grid: { - type: 'void', - 'x-component': 'Grid', - 'x-initializer': 'RecordBlockInitializers', - properties: {}, - }, - }, + 'x-component': 'Grid', + 'x-initializer': 'RecordBlockInitializers', + properties: {}, }, }, }, @@ -203,58 +243,75 @@ export const TableActionColumnInitializers = (props: any) => { }, }, }, - { - type: 'item', - title: '{{t("Update record")}}', - component: 'CustomizeActionInitializer', - schema: { - title: '{{t("Update record")}}', - 'x-component': 'Action.Link', - 'x-action': 'customize:update', - 'x-decorator': 'ACLActionProvider', - 'x-acl-action': 'update', - 'x-designer': 'Action.Designer', - 'x-action-settings': { - assignedValues: {}, - onSuccess: { - manualClose: true, - redirecting: false, - successMessage: '{{t("Updated successfully")}}', - }, - }, - 'x-component-props': { - useProps: '{{ useCustomizeUpdateActionProps }}', - }, - }, - visible: () => { - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - { - type: 'item', - title: '{{t("Custom request")}}', - component: 'CustomRequestInitializer', - schema: { - 'x-action': 'customize:table:request', - }, - visible: () => { - return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; - }, - }, - ], - }, - { - type: 'divider', + }, }, { type: 'item', - title: t('Column width'), - component: Resizable, + title: '{{t("Update record")}}', + name: 'updateRecord', + Component: 'CustomizeActionInitializer', + schema: { + title: '{{t("Update record")}}', + 'x-component': 'Action.Link', + 'x-action': 'customize:update', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action': 'update', + 'x-designer': 'Action.Designer', + 'x-action-settings': { + assignedValues: {}, + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: '{{t("Updated successfully")}}', + }, + }, + 'x-component-props': { + useProps: '{{ useCustomizeUpdateActionProps }}', + }, + }, + useVisible() { + const collection = useCollection(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, }, - ]} - component={ - - } - /> - ); -}; + { + type: 'item', + title: '{{t("Custom request")}}', + name: 'customRequest', + Component: 'CustomizeActionInitializer', + schema: { + title: '{{ t("Custom request") }}', + 'x-component': 'Action.Link', + 'x-action': 'customize:table:request', + 'x-designer': 'Action.Designer', + 'x-action-settings': { + requestSettings: {}, + onSuccess: { + manualClose: false, + redirecting: false, + successMessage: '{{t("Request success")}}', + }, + }, + 'x-component-props': { + useProps: '{{ useCustomizeRequestActionProps }}', + }, + }, + useVisible() { + const collection = useCollection(); + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + ], + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'item', + name: 'columnWidth', + title: 't("Column width")', + Component: Resizable, + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx index a0daa0c5ee..5b789bd858 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -1,8 +1,10 @@ import { useFieldSchema } from '@formily/react'; import { useCollection } from '../../'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; // 表格操作配置 -export const TableActionInitializers = { +export const tableActionInitializers = new SchemaInitializer({ + name: 'TableActionInitializers', title: "{{t('Configure actions')}}", icon: 'SettingOutlined', style: { @@ -11,12 +13,14 @@ export const TableActionInitializers = { items: [ { type: 'itemGroup', + name: 'enableActions', title: "{{t('Enable actions')}}", children: [ { type: 'item', + name: 'filter', title: "{{t('Filter')}}", - component: 'FilterActionInitializer', + Component: 'FilterActionInitializer', schema: { 'x-align': 'left', }, @@ -24,7 +28,8 @@ export const TableActionInitializers = { { type: 'item', title: "{{t('Add new')}}", - component: 'CreateActionInitializer', + name: 'addNew', + Component: 'CreateActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -32,7 +37,7 @@ export const TableActionInitializers = { skipScopeCheck: true, }, }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return !['view', 'file', 'sql'].includes(collection.template) || collection?.writableView; }, @@ -40,12 +45,13 @@ export const TableActionInitializers = { { type: 'item', title: "{{t('Delete')}}", - component: 'BulkDestroyActionInitializer', + name: 'delete', + Component: 'BulkDestroyActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', }, - visible: function useVisible() { + useVisible() { const collection = useCollection(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, @@ -53,19 +59,20 @@ export const TableActionInitializers = { { type: 'item', title: "{{t('Refresh')}}", - component: 'RefreshActionInitializer', + name: 'refresh', + Component: 'RefreshActionInitializer', schema: { 'x-align': 'right', }, }, { - type: 'item', + name: 'toggle', title: "{{t('Expand/Collapse')}}", - component: 'ExpandActionInitializer', + Component: 'ExpandActionInitializer', schema: { 'x-align': 'right', }, - visible: function useVisible() { + useVisible() { const schema = useFieldSchema(); const collection = useCollection(); const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; @@ -75,47 +82,23 @@ export const TableActionInitializers = { ], }, { + name: 'divider', type: 'divider', - visible: function useVisible() { + useVisible() { const collection = useCollection(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, }, - // { - // type: 'item', - // title: "{{t('Association fields filter')}}", - // component: 'ActionBarAssociationFilterAction', - // schema: { - // 'x-align': 'left', - // }, - // find: (schema: Schema) => { - // const resultSchema = Object.entries(schema.parent.properties).find( - // ([, value]) => value['x-component'] === 'AssociationFilter', - // )?.[1]; - // return resultSchema; - // }, - // visible: () => { - // const collection = useCollection(); - // const schema = useFieldSchema(); - // return (collection as any).template !== 'view' && schema['x-initializer'] !== 'GanttActionInitializers'; - // }, - // }, - // { - // type: 'divider', - // visible: () => { - // const collection = useCollection(); - // const schema = useFieldSchema(); - // return (collection as any).template !== 'view' && schema['x-initializer'] !== 'GanttActionInitializers'; - // }, - // }, { type: 'subMenu', + name: 'customize', title: '{{t("Customize")}}', children: [ { type: 'item', title: '{{t("Bulk update")}}', - component: 'CustomizeActionInitializer', + Component: 'CustomizeActionInitializer', + name: 'bulkUpdate', schema: { type: 'void', title: '{{ t("Bulk update") }}', @@ -146,7 +129,8 @@ export const TableActionInitializers = { { type: 'item', title: '{{t("Bulk edit")}}', - component: 'CustomizeBulkEditActionInitializer', + name: 'bulkEdit', + Component: 'CustomizeBulkEditActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -159,7 +143,8 @@ export const TableActionInitializers = { { type: 'item', title: '{{t("Add record")}}', - component: 'CustomizeAddRecordActionInitializer', + name: 'addRecord', + Component: 'CustomizeAddRecordActionInitializer', schema: { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', @@ -170,10 +155,10 @@ export const TableActionInitializers = { }, }, ], - visible: function useVisible() { + useVisible() { const collection = useCollection(); return !['view', 'sql'].includes(collection.template) || collection?.writableView; }, }, ], -}; +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx index 09a7fd6f4d..ce8894ba25 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableColumnInitializers.tsx @@ -1,95 +1,98 @@ -import { useField, useFieldSchema } from '@formily/react'; -import React from 'react'; +import { useFieldSchema } from '@formily/react'; +import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerChildren } from '../../application'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { useCompile } from '../../schema-component'; -import { SchemaInitializer } from '../SchemaInitializer'; import { - itemsMerge, useAssociatedTableColumnInitializerFields, useInheritsTableColumnInitializerFields, useTableColumnInitializerFields, } from '../utils'; // 表格列配置 -export const TableColumnInitializers = (props: any) => { - const { action = true } = props; - const { t } = useTranslation(); - const field = useField(); - const fieldSchema = useFieldSchema(); - const associatedFields = useAssociatedTableColumnInitializerFields(); +const ParentCollectionFields = () => { const inheritFields = useInheritsTableColumnInitializerFields(); + const { t } = useTranslation(); const compile = useCompile(); - const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; - const fieldItems: any[] = [ - { - type: 'itemGroup', - title: t('Display fields'), - children: useTableColumnInitializerFields(), - }, - ]; - if (inheritFields?.length > 0) { - inheritFields.forEach((inherit) => { - Object.values(inherit)[0].length && - fieldItems.push( - { - type: 'divider', - }, - { - type: 'itemGroup', - title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', - children: Object.values(inherit)[0].filter((v: any) => !v?.field?.isForeignKey), - }, - ); - }); - } - if (associatedFields?.length > 0 && (!isSubTable || field.readPretty)) { - fieldItems.push( - { - type: 'divider', - }, + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0].filter((v: any) => !v?.field?.isForeignKey), + }); + }); + return {res}; +}; + +const AssociatedFields = () => { + const associatedFields = useAssociatedTableColumnInitializerFields(); + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const schema: any = useMemo( + () => [ { type: 'itemGroup', title: t('Display association fields'), children: associatedFields, }, - ); - } - if (action) { - fieldItems.push( - { - type: 'divider', - }, - { - type: 'item', - title: t('Action column'), - component: 'TableActionColumnInitializer', - }, - ); - } - - return ( - { - if (s['x-action-column']) { - return s; - } - return { - type: 'void', - 'x-decorator': 'TableV2.Column.Decorator', - 'x-designer': 'TableV2.Column.Designer', - 'x-component': 'TableV2.Column', - properties: { - [s.name]: { - ...s, - }, - }, - }; - }} - items={itemsMerge(fieldItems)} - > - {t('Configure columns')} - + ], + [associatedFields, t], ); + if (!associatedFields?.length || fieldSchema['x-component'] === 'AssociationField.SubTable') return null; + return {schema}; }; + +export const tableColumnInitializers = new SchemaInitializer({ + name: 'TableColumnInitializers', + insertPosition: 'beforeEnd', + icon: 'SettingOutlined', + title: '{{t("Configure columns")}}', + wrap: (s) => { + if (s['x-action-column']) { + return s; + } + return { + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-designer': 'TableV2.Column.Designer', + 'x-component': 'TableV2.Column', + properties: { + [s.name]: { + ...s, + }, + }, + }; + }, + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: '{{t("Display fields")}}', + // children: DisplayFields, + useChildren: useTableColumnInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + { + name: 'associationFields', + Component: AssociatedFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + type: 'item', + name: 'add', + title: '{{t("Action column")}}', + Component: 'TableActionColumnInitializer', + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx index ad304148ab..616c20b17e 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableSelectorInitializers.tsx @@ -1,79 +1,68 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { SchemaInitializer, useCollection } from '../..'; +import { useCollection } from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; import { gridRowColWrap } from '../utils'; -export const TableSelectorInitializers = (props: any) => { - const { t } = useTranslation(); - const { name } = useCollection(); - const { insertPosition, component } = props; - - return ( - - ); -}; + ], + }, + ], +}); diff --git a/packages/core/client/src/schema-initializer/demos/basic.tsx b/packages/core/client/src/schema-initializer/demos/basic.tsx new file mode 100644 index 0000000000..9153f1f3a8 --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/basic.tsx @@ -0,0 +1,68 @@ +import { + Application, + CSSVariableProvider, + Plugin, + SchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import React from 'react'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + // 正常情况下这个值为 false,通过点击页面左上角的设计按钮切换,这里为了显示设置为 true + designable: true, + // 按钮标题标题 + title: 'Button Text', + // 调用 initializer.render() 时会渲染 items 列表 + items: [ + { + name: 'demo1', // 唯一标识 + Component: () =>
myInitializer content
, // 渲染组件 + }, + { + name: 'demo2', + Component: () =>
myInitializer content 2
, + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + // 注册 schema initializer + this.app.schemaInitializerManager.add(myInitializer); + // 注册路由 + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +class MyPlugin2 extends Plugin { + async load() { + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + + // 添加或者修改 schema initializer 的 items + myInitializer.add('demo3', { + Component: () =>
myInitializer content3
, + }); + + // 移除 demo2 + myInitializer.remove('demo2'); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin, MyPlugin2], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/build-type.tsx b/packages/core/client/src/schema-initializer/demos/build-type.tsx new file mode 100644 index 0000000000..13b66069d5 --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/build-type.tsx @@ -0,0 +1,86 @@ +import { + Application, + Plugin, + SchemaComponentPlugin, + SchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import React from 'react'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + designable: true, + title: 'Button Text', + items: [ + { + name: 'a', + type: 'itemGroup', // 渲染成类似 MenuGroup 的样式 + title: 'Group a', // group 标题 + children: [ + { + name: 'a1', + type: 'item', // 渲染成 Div + title 的组件 + title: 'A 1', + // 其他属性 + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ], + }, + { + name: 'divider', + type: 'divider', // 会渲染成分割线 + }, + { + name: 'b', + type: 'subMenu', // 会渲染成 带 Children 的 Menu 组件 + title: 'Group B', + children: [ + { + name: 'b1', + type: 'item', + title: 'B 1', + onClick: () => { + alert('b-1'); + }, + }, + { + name: 'b2', + type: 'item', + title: 'B 2', + }, + ], + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin, SchemaComponentPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/custom-button.tsx b/packages/core/client/src/schema-initializer/demos/custom-button.tsx new file mode 100644 index 0000000000..2c39c5da7e --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/custom-button.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Application, Plugin, SchemaInitializer, useSchemaInitializerRender } from '@nocobase/client'; +import { PlusOutlined } from '@ant-design/icons'; +import { Divider, Avatar, AvatarProps } from 'antd'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + designable: true, + // 使用自定义组件代替默认的 Button + Component: (props) => ( + + C + + ), + // 传递给 Component 的 props + componentProps: { + size: 'large', + }, + items: [ + { + name: 'hello', + Component: () =>
hello
, + }, + ], +}); + +const Root = () => { + const { exists, render } = useSchemaInitializerRender('MyInitializer'); + if (!exists) return null; + return ( +
+
+
初始化时自定义 Component
+ {render()} +
+ +
+
通过 render 更改
+ {render({ Component: () => })} +
+ +
+
不使用 Popover
+ {render({ popover: false, componentProps: { onClick: () => alert('test') } })} +
+
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/custom-items-component.tsx b/packages/core/client/src/schema-initializer/demos/custom-items-component.tsx new file mode 100644 index 0000000000..e883552494 --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/custom-items-component.tsx @@ -0,0 +1,92 @@ +import React, { FC } from 'react'; +import { + Application, + Plugin, + SchemaInitializer, + SchemaInitializerItemsProps, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { ButtonProps, Card, Divider, List, ListProps, Menu, MenuProps } from 'antd'; + +// 自定义 Items 渲染为 Menu +const CustomItemsMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ({ key: item.name, label: item.title, onClick: item.onClick }))} + > + ); +}; + +const CustomListGridMenu: FC>> = (props) => { + const { items, options, ...others } = props; + return ( + ( + + + {item.name} - {item.title} + + + )} + > + ); +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + designable: true, + title: 'Button Text', + ItemsComponent: CustomItemsMenu, + items: [ + { + name: 'a', + title: 'item a', + onClick: () => { + alert('a1'); + }, + }, + { + name: 'b', + title: 'item b', + }, + ], +}); + +const Root = () => { + const { exists, render } = useSchemaInitializerRender('MyInitializer'); + if (!exists) return null; + return ( +
+
渲染为 Menu
+ {render()} + +
渲染为 List Grid
+ {render({ ItemsComponent: CustomListGridMenu, itemsComponentStyle: { width: 300 } })} +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/demo1.tsx b/packages/core/client/src/schema-initializer/demos/demo1.tsx deleted file mode 100644 index a8b5824a74..0000000000 --- a/packages/core/client/src/schema-initializer/demos/demo1.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { TableOutlined } from '@ant-design/icons'; -import { Field } from '@formily/core'; -import { observer, useField } from '@formily/react'; -import { - SchemaComponent, - SchemaComponentProvider, - SchemaInitializer, - SchemaInitializerProvider, - useSchemaInitializer, -} from '@nocobase/client'; -import React from 'react'; - -const Hello = observer( - (props) => { - const field = useField(); - return ( -
- {field.title} -
- ); - }, - { displayName: 'Hello' }, -); - -const TableBlockInitializer = 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 ( - } - items={items} - onClick={({ item }) => { - // 如果有 items 时,onClick 里会返回点击的 item - // TODO: 实际情况,这里还需要补充更完整的初始化逻辑 - insert({ - type: 'void', - title: item.title, - 'x-component': 'Hello', - }); - }} - /> - ); -}); - -const initializers = { - AddBlock: { - title: 'Add block', - insertPosition: 'beforeBegin', - items: [ - { - type: 'itemGroup', - title: 'Data blocks', - children: [ - { - type: 'item', - title: 'Table', - component: 'TableBlockInitializer', - }, - { - type: 'item', - title: 'Form', - component: 'GeneralInitializer', - schema: { - type: 'void', - title: 'Form', - 'x-component': 'Hello', - }, - }, - ], - }, - ], - }, -}; - -const AddBlockButton = observer( - (props: any) => { - const { render } = useSchemaInitializer('AddBlock'); - return <>{render()}; - }, - { displayName: 'AddBlockButton' }, -); - -export default function App() { - return ( - - - - - - ); -} diff --git a/packages/core/client/src/schema-initializer/demos/demo2.tsx b/packages/core/client/src/schema-initializer/demos/demo2.tsx deleted file mode 100644 index debbc5af74..0000000000 --- a/packages/core/client/src/schema-initializer/demos/demo2.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - Action, - ActionBar, - SchemaComponent, - SchemaComponentProvider, - SchemaInitializerProvider, -} from '@nocobase/client'; -import React from 'react'; - -const initializers = { - AddAction: { - title: 'Configure actions', - insertPosition: 'beforeEnd', - style: { marginLeft: 8 }, - items: [ - { - type: 'itemGroup', - title: 'Enable actions', - children: [ - { - type: 'item', - title: 'Create', - component: 'ActionInitializer', - schema: { - title: 'Create', - 'x-action': 'posts:create', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-align': 'left', - }, - }, - { - type: 'item', - title: 'Update', - component: 'ActionInitializer', - schema: { - title: 'Update', - 'x-action': 'posts:update', - 'x-component': 'Action', - 'x-designer': 'Action.Designer', - 'x-align': 'right', - }, - }, - ], - }, - ], - }, -}; - -export default function App() { - return ( - - - - - - ); -} diff --git a/packages/core/client/src/schema-initializer/demos/demo4.tsx b/packages/core/client/src/schema-initializer/demos/demo4.tsx deleted file mode 100644 index db27826808..0000000000 --- a/packages/core/client/src/schema-initializer/demos/demo4.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { ArrayTable } from '@formily/antd-v5'; -import { uid } from '@formily/shared'; -import { - Input, - SchemaComponent, - SchemaComponentProvider, - SchemaInitializerItemOptions, - SchemaInitializerProvider, -} from '@nocobase/client'; -import React from 'react'; - -const removeColumn = (schema, cb) => { - cb(schema, { - removeParentsIfNoChildren: true, - breakRemoveOn: { - 'x-component': 'ArrayTable', - }, - }); -}; - -const useTableColumnInitializerFields = () => { - const fields: SchemaInitializerItemOptions[] = [ - { - type: 'item', - title: 'Name', - remove: removeColumn, - schema: { - name: 'name', - type: 'string', - title: 'Name', - 'x-collection-field': 'posts.name', - 'x-component': 'Input', - 'x-read-pretty': true, - }, - component: 'CollectionFieldInitializer', - }, - { - type: 'item', - title: 'Title', - remove: removeColumn, - schema: { - name: 'title', - type: 'string', - title: 'Title', - 'x-collection-field': 'posts.title', - 'x-component': 'Input', - 'x-read-pretty': true, - }, - component: 'CollectionFieldInitializer', - }, - ]; - return fields; -}; - -const columnWrap = (s) => { - return { - name: [uid()], - type: 'void', - title: s.title, - 'x-component': 'ArrayTable.Column', - properties: { - [s.name]: { - ...s, - }, - }, - }; -}; - -// 因为有个动态获取字段的 hook,所以这里将 SchemaInitializerProvider 自定义的处理了一下 -const CustomSchemaInitializerProvider: React.FC = (props) => { - const initializers = { - AddColumn: { - insertPosition: 'beforeEnd', - title: 'Configure columns', - wrap: columnWrap, - items: [ - { - type: 'itemGroup', - title: 'Display fields', - children: useTableColumnInitializerFields(), - }, - ], - }, - }; - return {props.children}; -}; - -const schema = { - type: 'object', - properties: { - input: { - type: 'array', - default: [ - { id: 1, name: 'Name1' }, - { id: 2, name: 'Name2' }, - { id: 3, name: 'Name3' }, - ], - 'x-component': 'ArrayTable', - 'x-initializer': 'AddColumn', - 'x-component-props': { - rowKey: 'id', - }, - properties: { - column1: { - type: 'void', - title: 'Name', - 'x-component': 'ArrayTable.Column', - properties: { - name: { - type: 'string', - 'x-component': 'Input', - 'x-collection-field': 'posts.name', - 'x-read-pretty': true, - }, - }, - }, - }, - }, - }, -}; - -export default function App() { - return ( - - - - - - ); -} diff --git a/packages/core/client/src/schema-initializer/demos/dynamic-visible-children.tsx b/packages/core/client/src/schema-initializer/demos/dynamic-visible-children.tsx new file mode 100644 index 0000000000..94c04554d1 --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/dynamic-visible-children.tsx @@ -0,0 +1,84 @@ +import { + Application, + Plugin, + SchemaComponentPlugin, + SchemaInitializer, + useSchemaInitializerRender, +} from '@nocobase/client'; +import React from 'react'; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + designable: true, + title: 'Button Text', + items: [ + { + name: 'a', + type: 'itemGroup', + title: 'Group a', + // 动态加载子项 + useChildren() { + return [ + { + name: 'a1', + type: 'item', + title: 'A 1', + onClick: () => { + alert('a-1'); + }, + }, + { + name: 'a2', + type: 'item', + title: 'A 2', + }, + ]; + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'b', + type: 'item', + title: 'Item B', + useVisible() { + return false; + }, + }, + { + name: 'c', + type: 'item', + title: 'Item C', + useVisible() { + return true; + }, + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin, SchemaComponentPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/insert-schema-action.tsx b/packages/core/client/src/schema-initializer/demos/insert-schema-action.tsx new file mode 100644 index 0000000000..7a273c084f --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/insert-schema-action.tsx @@ -0,0 +1,103 @@ +import { + Action, + ActionBar, + ActionInitializer, + Application, + Plugin, + SchemaComponent, + SchemaComponentProvider, + SchemaInitializer, + useApp, +} from '@nocobase/client'; +import React from 'react'; + +const addActionInitializer = new SchemaInitializer({ + name: 'AddAction', + title: 'Configure actions', + // 插入位置 + insertPosition: 'beforeEnd', + style: { marginLeft: 8 }, + items: [ + { + name: 'enableActions', + type: 'itemGroup', + title: 'Enable actions', + children: [ + { + name: 'create', + title: 'Create', + Component: 'ActionInitializer', + schema: { + title: 'Create', + 'x-action': 'posts:create', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-align': 'left', + }, + }, + { + name: 'update', + title: 'Update', + Component: 'ActionInitializer', + schema: { + title: 'Update', + 'x-action': 'posts:update', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-align': 'right', + }, + }, + ], + }, + ], +}); + +const Root = () => { + return ( +
+ + + +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(addActionInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/insert-schema-basic.tsx b/packages/core/client/src/schema-initializer/demos/insert-schema-basic.tsx new file mode 100644 index 0000000000..6da9a5cdcf --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/insert-schema-basic.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaComponentProvider, + SchemaInitializer, + useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { observer, useField } from '@formily/react'; +import { Field } from '@formily/core'; + +const Hello = observer(() => { + const field = useField(); + return ( +
+ {field.title} +
+ ); +}); + +function Demo() { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + title: itemConfig.title, + 'x-component': 'Hello', + }); + }; + return
{itemConfig.title}
; +} + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + title: 'Add Block', + // 插入位置 + insertPosition: 'beforeEnd', + items: [ + { + name: 'a', + title: 'Item A', + Component: Demo, + }, + { + name: 'b', + title: 'Item B', + Component: Demo, + }, + ], +}); + +const AddBlockButton = observer(() => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return render(); +}); + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( +
+ + + +
+ ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/demo3.tsx b/packages/core/client/src/schema-initializer/demos/insert-schema-form-item.tsx similarity index 50% rename from packages/core/client/src/schema-initializer/demos/demo3.tsx rename to packages/core/client/src/schema-initializer/demos/insert-schema-form-item.tsx index b337c26ed8..9368462c9e 100644 --- a/packages/core/client/src/schema-initializer/demos/demo3.tsx +++ b/packages/core/client/src/schema-initializer/demos/insert-schema-form-item.tsx @@ -1,23 +1,28 @@ import { + Application, + CollectionFieldInitializer, Form, FormItem, + Input, Markdown, + Plugin, SchemaComponent, SchemaComponentProvider, SchemaInitializer, - SchemaInitializerItemOptions, - SchemaInitializerProvider, + SchemaInitializerItem, + SchemaInitializerItemType, useSchemaInitializer, + useSchemaInitializerItem, + useSchemaInitializerRender, } from '@nocobase/client'; -import { Input } from 'antd'; -import React from 'react'; +import React, { FC } from 'react'; const useFormItemInitializerFields = () => { return [ { - type: 'item', + name: 'name', title: 'Name', - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', schema: { type: 'string', title: 'Name', @@ -28,9 +33,9 @@ const useFormItemInitializerFields = () => { }, }, { - type: 'item', + name: 'title', title: 'Title', - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', schema: { type: 'string', title: 'Title', @@ -40,13 +45,14 @@ const useFormItemInitializerFields = () => { 'x-collection-field': 'posts.title', }, }, - ] as SchemaInitializerItemOptions[]; + ] as SchemaInitializerItemType[]; }; -const TextInitializer = SchemaInitializer.itemWrap((props) => { - const { insert } = props; +const TextInitializer: FC = () => { + const { insert } = useSchemaInitializer(); + const itemConfig = useSchemaInitializerItem(); return ( - { insert({ type: 'void', @@ -55,12 +61,37 @@ const TextInitializer = SchemaInitializer.itemWrap((props) => { // 'x-editable': false, }); }} + {...itemConfig} /> ); +}; + +const addFormItemInitializer = new SchemaInitializer({ + name: 'AddFormItem', + title: 'Configure actions', + insertPosition: 'beforeEnd', + items: [ + { + name: 'displayFields', + type: 'itemGroup', + title: 'Display fields', + // 通过 useChildren 来动态加载子项 + useChildren: useFormItemInitializerFields, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'addText', + title: 'Add text', + Component: TextInitializer, + }, + ], }); const Page = (props) => { - const { render } = useSchemaInitializer('AddFormItem'); + const { render } = useSchemaInitializerRender('AddFormItem'); return (
{props.children} @@ -69,33 +100,12 @@ const Page = (props) => { ); }; -export default function App() { - const initializers = { - AddFormItem: { - title: 'Configure fields', - insertPosition: 'beforeEnd', - items: [ - { - type: 'itemGroup', - title: 'Display fields', - // 从 hook 动态加载字段 - children: useFormItemInitializerFields(), - }, - { - type: 'divider', - }, - { - type: 'item', - title: 'Add text', - component: 'TextInitializer', - }, - ], - }, - }; +const Root = () => { return ( - - +
+ - - + > + +
); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(addFormItemInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } } + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/demos/nested-items.tsx b/packages/core/client/src/schema-initializer/demos/nested-items.tsx new file mode 100644 index 0000000000..187eb5faca --- /dev/null +++ b/packages/core/client/src/schema-initializer/demos/nested-items.tsx @@ -0,0 +1,138 @@ +import React, { FC } from 'react'; +import { + Application, + Plugin, + SchemaInitializer, + SchemaInitializerItemType, + SchemaInitializerChildren, + useSchemaInitializerItem, + SchemaInitializerChild, + useSchemaInitializerRender, +} from '@nocobase/client'; +import { Divider, Menu } from 'antd'; + +// TODO:这里要加上 Context +const ParentA: FC<{ children: SchemaInitializerItemType[] }> = ({ children }) => { + return ( +
+
parent
+ + + {/* 可以自行决定需要渲染效果 */} + + {/* 示例1:直接渲染 */} +
直接渲染
+ {children.map((item) => ( + + ))} + + + + {/* 等同于 */} +
使用内置的 SchemaInitializerChildren
+ {children} + + + + {/* 示例2:渲染成列表 */} +
渲染成列表
+
    + {children.map((item) => { + return ( +
  • +
    name: {item.name}
    + +
  • + ); + })} +
+ + + {/* 示例3:渲染成 Menu 形式 */} +
渲染成 Menu 形式
+ ({ + key: item.name, + label: , + }))} + /> +
+ ); +}; + +// 配置项的内容会被当做 props 传入到 Component 中 +// TODO: 重新完善 demo +const Demo = () => { + const itemConfig = useSchemaInitializerItem(); + const { onClick, title } = itemConfig; + return
{title}
; +}; + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + designable: true, + title: 'Button Text', + items: [ + { + name: 'a', + Component: ParentA, + children: [ + { + name: 'a1', + title: 'a1 title', + onClick: () => { + alert('test'); + }, + Component: Demo, + }, + { + name: 'a2', + Component: () =>
a2
, + }, + { + name: 'a3', + Component: () =>
a3
, + }, + ], + }, + ], +}); + +const Root = () => { + const { render } = useSchemaInitializerRender('MyInitializer'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaInitializerManager.add(myInitializer); + this.app.router.add('root', { + path: '/', + Component: Root, + }); + } +} + +class MyPlugin2 extends Plugin { + async load() { + const myInitializer = this.app.schemaInitializerManager.get('MyInitializer'); + + // 嵌套添加 + myInitializer.add('a.a4', { + Component: () =>
a4
, + }); + + // 嵌套移除 + myInitializer.remove('a.a3'); + } +} + +const app = new Application({ + router: { + type: 'memory', + initialEntries: ['/'], + }, + plugins: [MyPlugin, MyPlugin2], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts index 035736b6f2..1b35c1f09b 100644 --- a/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts +++ b/packages/core/client/src/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts @@ -12,6 +12,7 @@ export const useGetAriaLabelOfSchemaInitializer = () => { const { name } = useCollection(); const getAriaLabel = useCallback( (postfix?: string) => { + if (!fieldSchema) return ''; const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : ''; const collectionName = name ? `-${name}` : ''; postfix = postfix ? `-${postfix}` : ''; diff --git a/packages/core/client/src/schema-initializer/index.md b/packages/core/client/src/schema-initializer/index.md index 5f22dad352..c2c63439af 100644 --- a/packages/core/client/src/schema-initializer/index.md +++ b/packages/core/client/src/schema-initializer/index.md @@ -24,97 +24,48 @@ group: } ``` -SchemaInitializer 的核心包括 `` 和 `` 两个组件。`` 用于创建 Schema 的下拉菜单按钮,下拉菜单的菜单项为 ``。 - -SchemaInitializer.Item 用于实现各种初始化器(Initializer),负责各种 schema 的初始化逻辑,可以是区块、字段、操作等 schema 片段。目前内置的 Initializer 有: - -- `ActionInitializer` 普通的 Action 操作按钮的初始化器 -- `AddNewActionInitializer` 添加按钮的初始化器 -- `CalendarBlockInitializer` 日历区块的初始化器 -- `CollectionFieldInitializer` 字段的初始化器 -- `FormBlockInitializer` 表单的初始化器 -- `GeneralInitializer` 通用的初始化器 -- `MarkdownBlockInitializer` Markdown 区块的初始化器 -- `TableBlockInitializer` 表格区块的初始化器 - -SchemaInitializer.Button 用于将各种 Initializer 分组,以下拉菜单的方式呈现。内置的有: - -- `BlockInitializers` 页面里的「添加区块」 -- `CalendarActionInitializers` 日历的「操作配置」 -- `DetailsActionInitializers` 详情的「操作配置」 -- `FormActionInitializers` 普通表单的「操作配置」 -- `GridFormItemInitializers` Grid 组件里「配置字段」 -- `MenuItemInitializers` 菜单里「添加菜单项」 -- `PopupFormActionInitializers` 弹窗表单的「操作配置」 -- `RecordBlockInitializers` 当前行记录所在面板的「添加区块」 -- `TableActionInitializers` 表格「操作配置」 -- `TableColumnInitializers` 表格「列配置」 -- `TableRecordActionInitializers` 表格当前行记录的「操作配置」 - -## 配置 - -```tsx | pure -const initializers = { - // 可以是 SchemaInitializer.Button 的 props - BlockInitializers: { - title: 'Add new', - items: [ - { - type: 'itemGroup', - title: 'Data blocks', - children: [ - { - type: 'item', - title: 'Table', - component: 'TableBlockInitializer', - }, - { - type: 'item', - title: 'Form', - component: 'FormBlockInitializer', - }, - ], - }, - { - type: 'itemGroup', - title: 'Media', - children: [ - { - type: 'item', - title: 'Markdown', - component: 'MarkdownBlockInitializer', - }, - ], - }, - ], - }, - // 也可以是自定义的 SchemaInitializer.Button 组件 - CustomSchemaInitializerButton, -}; - -const CustomSchemaInitializerButton = () => { - return -} - - - {/* children */} - -``` - ## Examples -### Block +### Basic - + -### Action +### Nested items - + -### FormItem +### Custom Items Component - +列表默认使用 `List` 组件,可以通过 `ItemsComponent` 属性自定义列表组件。 -### Table.Column + - +### Custom Button + + + +### Built Type + +NocoBase 提供了几个内置的组件,可以直接使用。 + + + +### Dynamic visible & children + +动态显示和隐藏 Item 项,以及动态加载 children。 + + + +### Insert schema + +#### Basic + + + +#### Action + + + +#### FormItem + + diff --git a/packages/core/client/src/schema-initializer/index.ts b/packages/core/client/src/schema-initializer/index.ts index a519bbb88a..f85e0538af 100644 --- a/packages/core/client/src/schema-initializer/index.ts +++ b/packages/core/client/src/schema-initializer/index.ts @@ -1,8 +1,8 @@ +import * as initializerComponents from './components'; +import * as items from './items'; +export { useCurrentSchema } from './utils'; export * from './buttons'; export * from './items'; -export * from './SchemaInitializer'; -export * from './SchemaInitializerProvider'; -export * from './types'; export { createFilterFormBlockSchema, createFormBlockSchema, @@ -10,6 +10,7 @@ export { createTableBlockSchema, gridRowColWrap, itemsMerge, + useCollectionDataSourceItemsV2, useAssociatedTableColumnInitializerFields, useCollectionDataSourceItems, useInheritsTableColumnInitializerFields, @@ -18,10 +19,83 @@ export { } from './utils'; import { Plugin } from '../application/Plugin'; -import { SchemaInitializerProvider } from './SchemaInitializerProvider'; +import { + bulkEditFormItemInitializers, + calendarActionInitializers, + calendarFormActionInitializers, + createFormBlockInitializers, + createFormBulkEditBlockInitializers, + cusomeizeCreateFormBlockInitializers, + customFormItemInitializers, + filterFormActionInitializers, + createFormActionInitializers, + updateFormActionInitializers, + bulkEditFormActionInitializers, + ganttActionInitializers, + filterFormItemInitializers, + gridCardActionInitializers, + gridCardItemActionInitializers, + kanbanActionInitializers, + listActionInitializers, + listItemActionInitializers, + recordBlockInitializers, + recordFormBlockInitializers, + subTableActionInitializers, + tableSelectorInitializers, + tabPaneInitializers, + tabPaneInitializersForRecordBlock, + tabPaneInitializersForBulkEditFormBlock, + blockInitializers, + tableActionColumnInitializers, + tableActionInitializers, + tableColumnInitializers, + formItemInitializers, + formActionInitializers, + readPrettyFormItemInitializers, + detailsActionInitializers, + readPrettyFormActionInitializers, +} from './buttons'; -export class SchemaInitializerPlugin extends Plugin { +export class SchemaInitializerPlugin extends Plugin { async load() { - this.app.use(SchemaInitializerProvider, this.options?.config); + this.app.addComponents({ + ...initializerComponents, + ...items, + } as any); + + this.app.schemaInitializerManager.add(blockInitializers); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaInitializerManager.add(tableActionColumnInitializers); + this.app.schemaInitializerManager.add(formItemInitializers); + this.app.schemaInitializerManager.add(formActionInitializers); + this.app.schemaInitializerManager.add(detailsActionInitializers); + this.app.schemaInitializerManager.add(readPrettyFormItemInitializers); + this.app.schemaInitializerManager.add(readPrettyFormActionInitializers); + this.app.schemaInitializerManager.add(bulkEditFormItemInitializers); + this.app.schemaInitializerManager.add(calendarActionInitializers); + this.app.schemaInitializerManager.add(calendarFormActionInitializers); + this.app.schemaInitializerManager.add(createFormBlockInitializers); + this.app.schemaInitializerManager.add(createFormBulkEditBlockInitializers); + this.app.schemaInitializerManager.add(cusomeizeCreateFormBlockInitializers); + this.app.schemaInitializerManager.add(customFormItemInitializers); + this.app.schemaInitializerManager.add(filterFormActionInitializers); + this.app.schemaInitializerManager.add(createFormActionInitializers); + this.app.schemaInitializerManager.add(updateFormActionInitializers); + this.app.schemaInitializerManager.add(bulkEditFormActionInitializers); + this.app.schemaInitializerManager.add(ganttActionInitializers); + this.app.schemaInitializerManager.add(filterFormItemInitializers); + this.app.schemaInitializerManager.add(gridCardActionInitializers); + this.app.schemaInitializerManager.add(gridCardItemActionInitializers); + this.app.schemaInitializerManager.add(kanbanActionInitializers); + this.app.schemaInitializerManager.add(listActionInitializers); + this.app.schemaInitializerManager.add(listItemActionInitializers); + this.app.schemaInitializerManager.add(recordBlockInitializers); + this.app.schemaInitializerManager.add(recordFormBlockInitializers); + this.app.schemaInitializerManager.add(subTableActionInitializers); + this.app.schemaInitializerManager.add(tableSelectorInitializers); + this.app.schemaInitializerManager.add(tabPaneInitializers); + this.app.schemaInitializerManager.add(tabPaneInitializersForRecordBlock); + this.app.schemaInitializerManager.add(tabPaneInitializersForBulkEditFormBlock); } } diff --git a/packages/core/client/src/schema-initializer/items/ActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/ActionInitializer.tsx index a33f56770d..61bbd311cf 100644 --- a/packages/core/client/src/schema-initializer/items/ActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/ActionInitializer.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { InitializerWithSwitch } from './InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../application'; export const ActionInitializer = (props) => { - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/BlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/BlockInitializer.tsx index 729fd2feb9..8c2c8edc53 100644 --- a/packages/core/client/src/schema-initializer/items/BlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/BlockInitializer.tsx @@ -1,12 +1,14 @@ import { merge } from '@formily/shared'; import React from 'react'; -import { SchemaInitializer } from '..'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; // Block export const BlockInitializer = (props) => { - const { item, schema, insert } = props; + const { item, schema, ...others } = props; + const { insert } = useSchemaInitializer(); return ( - { const s = merge(schema || {}, item.schema || {}); item?.schemaInitialize?.(s); @@ -15,3 +17,9 @@ export const BlockInitializer = (props) => { /> ); }; + +export const BlockItemInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { schema, ...others } = itemConfig; + return ; +}; diff --git a/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx index 637357f45e..00c017535c 100644 --- a/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CalendarBlockInitializer.tsx @@ -9,17 +9,19 @@ import { useGlobalTheme } from '../../global-theme'; import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; import { createCalendarBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const CalendarBlockInitializer = (props) => { - const { insert } = props; +export const CalendarBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); + const itemConfig = useSchemaInitializerItem(); return ( } onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx b/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx index 142f5c11d6..3336d3f931 100644 --- a/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CollectionFieldInitializer.tsx @@ -2,8 +2,10 @@ import { ISchema } from '@formily/react'; import React from 'react'; import { InitializerWithSwitch } from './InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../application'; -export const CollectionFieldInitializer = (props) => { +export const CollectionFieldInitializer = () => { const schema: ISchema = {}; - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx index c76de01747..ccb139b947 100644 --- a/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateActionInitializer.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { ActionInitializer } from './ActionInitializer'; +import { useSchemaInitializerItem } from '../../application'; -export const CreateActionInitializer = (props) => { +export const CreateActionInitializer = () => { const schema = { type: 'void', 'x-action': 'create', @@ -52,5 +53,6 @@ export const CreateActionInitializer = (props) => { }, }, }; - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx index 86b0093d51..32fabd3374 100644 --- a/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateFormBlockInitializer.tsx @@ -4,16 +4,19 @@ import { FormOutlined } from '@ant-design/icons'; import { useBlockAssociationContext } from '../../block-provider'; import { useCollection } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const CreateFormBlockInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +// TODO: `SchemaInitializerItem` items +export const CreateFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { insert } = useSchemaInitializer(); const association = useBlockAssociationContext(); const collection = useCollection(); return ( - } {...others} onClick={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/CreateFormBulkEditBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateFormBulkEditBlockInitializer.tsx index a783767355..73d86a2d2f 100644 --- a/packages/core/client/src/schema-initializer/items/CreateFormBulkEditBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateFormBulkEditBlockInitializer.tsx @@ -3,16 +3,18 @@ import React from 'react'; import { useBlockAssociationContext } from '../../block-provider'; import { useCollection } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const CreateFormBulkEditBlockInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const CreateFormBulkEditBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const association = useBlockAssociationContext(); const collection = useCollection(); return ( - } {...others} onClick={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx index c443766d72..d7dd564a27 100644 --- a/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CreateSubmitActionInitializer.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { useFieldSchema } from '@formily/react'; import { ActionInitializer } from './ActionInitializer'; export const CreateSubmitActionInitializer = (props) => { diff --git a/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx index 7c8620c32c..3be3df60a7 100644 --- a/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CustomizeActionInitializer.tsx @@ -2,13 +2,17 @@ import React from 'react'; import { BlockInitializer } from '.'; import { useCollection } from '../../collection-manager'; +import { useSchemaInitializerItem } from '../../application'; -export const CustomizeActionInitializer = (props) => { +export const CustomizeActionInitializer = () => { const collection = useCollection(); + const itemConfig = useSchemaInitializerItem(); + const schema = {}; if (collection && schema['x-acl-action']) { schema['x-acl-action'] = `${collection.name}:${schema['x-acl-action']}`; schema['x-decorator'] = 'ACLActionProvider'; } - return ; + + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx index 4c00836dd0..571ba4e3ce 100644 --- a/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CustomizeAddRecordActionInitializer.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { BlockInitializer } from './BlockInitializer'; +import { useSchemaInitializerItem } from '../../application'; -export const CustomizeAddRecordActionInitializer = (props) => { +export const CustomizeAddRecordActionInitializer = () => { const schema = { type: 'void', title: '{{t("Add record")}}', @@ -48,5 +49,7 @@ export const CustomizeAddRecordActionInitializer = (props) => { }, }, }; - return ; + const itemConfig = useSchemaInitializerItem(); + + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/CustomizeBulkEditActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomizeBulkEditActionInitializer.tsx index b35e5add12..18eee9ea8b 100644 --- a/packages/core/client/src/schema-initializer/items/CustomizeBulkEditActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CustomizeBulkEditActionInitializer.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { BlockInitializer } from './BlockInitializer'; +import { useSchemaInitializerItem } from '../../application'; -export const CustomizeBulkEditActionInitializer = (props) => { +export const CustomizeBulkEditActionInitializer = () => { const schema = { type: 'void', title: '{{t("Bulk edit")}}', @@ -51,5 +52,6 @@ export const CustomizeBulkEditActionInitializer = (props) => { }, }, }; - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx index 2a01b53cd3..3b35ca352c 100644 --- a/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/DataBlockInitializer.tsx @@ -1,43 +1,295 @@ -import { TableOutlined } from '@ant-design/icons'; -import React, { useContext } from 'react'; - -import { SchemaInitializer, SchemaInitializerButtonContext } from '..'; +import Icon, { TableOutlined } from '@ant-design/icons'; +import { Divider, Empty, Input, MenuProps, Spin } from 'antd'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + SchemaInitializerItem, + useSchemaInitializer, + useSchemaInitializerMenuItems, + SchemaInitializerMenu, +} from '../../application'; +import { useCompile } from '../../schema-component'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { useCollectionDataSourceItems } from '../utils'; +import { useCollectionDataSourceItemsV2 } from '../utils'; -export const DataBlockInitializer = (props) => { +const MENU_ITEM_HEIGHT = 40; +const STEP = 15; + +export const SearchCollections = ({ value: outValue, onChange }) => { + const { t } = useTranslation(); + const [value, setValue] = useState(outValue); + const inputRef = React.useRef(null); + + // 之所以要增加个内部的 value 是为了防止用户输入过快时造成卡顿的问题 + useEffect(() => { + setValue(outValue); + }, [outValue]); + + // TODO: antd 的 Input 的 autoFocus 有 BUG,会不生效,等待官方修复后再简化:https://github.com/ant-design/ant-design/issues/41239 + useEffect(() => { + // 1. 组件在第一次渲染时自动 focus,提高用户体验 + inputRef.current.input.focus(); + + // 2. 当组件已经渲染,并再次显示时,自动 focus + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + inputRef.current.input.focus(); + } + }); + + observer.observe(inputRef.current.input); + return () => { + observer.disconnect(); + }; + }, []); + + return ( +
e.stopPropagation()}> + { + e.stopPropagation(); + }} + onChange={(e) => { + onChange(e.target.value); + setValue(e.target.value); + }} + /> + +
+ ); +}; + +const LoadingItem = ({ loadMore, maxHeight }) => { + const spinRef = useRef(null); + useEffect(() => { + const el = spinRef.current; + if (!el) return; + + let container = el.parentElement; + while (container && container.tagName !== 'UL') { + container = container.parentElement; + } + + const checkLoadMore = function () { + if (!container) return; + // 判断滚动是否到达底部 + if (Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) <= MENU_ITEM_HEIGHT) { + // 到达底部,执行加载更多的操作 + loadMore(); + } + }; + + // 监听滚动,滚动到底部触发加载更多 + if (container) { + container.style.height = `${maxHeight - MENU_ITEM_HEIGHT}px`; + container.style.maxHeight = 'inherit'; + container.style.overflowY = 'scroll'; + container.addEventListener('scroll', checkLoadMore); + } + + return () => { + if (container) { + container.removeEventListener('scroll', checkLoadMore); + delete container.style.height; + } + }; + }, [loadMore, maxHeight]); + + return ( +
e.stopPropagation()}> + +
+ ); +}; + +export function useMenuSearch(items: any[], isOpenSubMenu: boolean, showType?: boolean) { + const [searchValue, setSearchValue] = useState(''); + const [count, setCount] = useState(STEP); + useEffect(() => { + if (isOpenSubMenu) { + setSearchValue(''); + } + }, [isOpenSubMenu]); + + // 根据搜索的值进行处理 + const searchedItems = useMemo(() => { + if (!searchValue) return items; + const lowerSearchValue = searchValue.toLocaleLowerCase(); + return items.filter( + (item) => + (item.label || item.title) && + String(item.label || item.title) + .toLocaleLowerCase() + .includes(lowerSearchValue), + ); + }, [searchValue, items]); + + const shouldLoadMore = useMemo(() => searchedItems.length > count, [count, searchedItems]); + + // 根据 count 进行懒加载处理 + const limitedSearchedItems = useMemo(() => { + return searchedItems.slice(0, count); + }, [searchedItems, count]); + + // 最终的返回结果 + const resultItems = useMemo(() => { + // isMenuType 为了 `useSchemaInitializerMenuItems()` 里面处理判断标识的 + const res: any[] = [ + // 开头:搜索框 + Object.assign( + { + key: 'search', + label: ( + { + setCount(STEP); + setSearchValue(val); + }} + /> + ), + onClick({ domEvent }) { + domEvent.stopPropagation(); + }, + }, + showType ? { isMenuType: true } : {}, + ), + ]; + + // 中间:搜索的数据 + if (limitedSearchedItems.length > 0) { + // 有搜索结果 + res.push(...limitedSearchedItems); + if (shouldLoadMore) { + res.push( + Object.assign( + { + key: 'load-more', + label: ( + { + setCount((count) => count + STEP); + }} + /> + ), + }, + showType ? { isMenuType: true } : {}, + ), + ); + } + } else { + // 搜索结果为空 + res.push( + Object.assign( + { + key: 'empty', + style: { + height: 150, + }, + label: ( +
e.stopPropagation()}> + +
+ ), + }, + showType ? { isMenuType: true } : {}, + ), + ); + } + + return res; + }, [limitedSearchedItems, searchValue, shouldLoadMore, showType]); + + return resultItems; +} + +export interface DataBlockInitializerProps { + templateWrap?: ( + templateSchema: any, + { + item, + }: { + item: any; + }, + ) => any; + onCreateBlockSchema?: (args: any) => void; + createBlockSchema?: (args: any) => any; + isCusomeizeCreate?: boolean; + icon?: string | React.ReactNode; + name: string; + title: string; + items?: any[]; + componentType: string; +} + +export const DataBlockInitializer = (props: DataBlockInitializerProps) => { const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, - insert, isCusomeizeCreate, + icon = TableOutlined, + name, + title, items, - ...others } = props; + const { insert } = useSchemaInitializer(); + const compile = useCompile(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { setVisible } = useContext(SchemaInitializerButtonContext); - const defaultItems = useCollectionDataSourceItems(componentType); - - return ( - } - {...others} - onClick={async ({ item }) => { - if (item.template) { - const s = await getTemplateSchemaByMode(item); - templateWrap ? insert(templateWrap(s, { item })) : insert(s); - } else { - if (onCreateBlockSchema) { - onCreateBlockSchema({ item }); - } else if (createBlockSchema) { - insert(createBlockSchema({ collection: item.name, isCusomeizeCreate })); - } + const onClick = useCallback( + async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + templateWrap ? insert(templateWrap(s, { item })) : insert(s); + } else { + if (onCreateBlockSchema) { + onCreateBlockSchema({ item }); + } else if (createBlockSchema) { + insert(createBlockSchema({ collection: item.collectionName || item.name, isCusomeizeCreate })); } - setVisible(false); - }} - items={items || defaultItems} - /> + } + }, + [createBlockSchema, getTemplateSchemaByMode, insert, isCusomeizeCreate, onCreateBlockSchema, templateWrap], ); + const defaultItems = useCollectionDataSourceItemsV2(componentType); + const menuChildren = useMemo(() => items || defaultItems, [items, defaultItems]); + const childItems = useSchemaInitializerMenuItems(menuChildren, name, onClick); + const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); + const searchedChildren = useMenuSearch(childItems, isOpenSubMenu); + const compiledMenuItems = useMemo( + () => [ + { + key: name, + label: compile(title), + icon: typeof icon === 'string' ? : icon, + onClick: (info) => { + if (info.key !== name) return; + onClick({ ...info, item: props }); + }, + children: searchedChildren, + }, + ], + [name, compile, title, icon, searchedChildren, onClick, props], + ); + + if (menuChildren.length > 0) { + return ( + { + setIsOpenSubMenu(keys.length > 0); + }} + items={compiledMenuItems} + /> + ); + } + + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx index 7c5a3ce70b..29fb7991b7 100644 --- a/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/DetailsBlockInitializer.tsx @@ -3,13 +3,15 @@ import React from 'react'; import { useCollectionManager } from '../../collection-manager'; import { createDetailsBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const DetailsBlockInitializer = (props) => { - const { insert } = props; +export const DetailsBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { getCollection } = useCollectionManager(); + const itemConfig = useSchemaInitializerItem(); return ( } componentType={'Details'} onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx index 451a09dcea..b980d50905 100644 --- a/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/FilterBlockInitializer.tsx @@ -1,34 +1,3 @@ -import { TableOutlined } from '@ant-design/icons'; -import React, { useContext } from 'react'; +import { DataBlockInitializer } from './DataBlockInitializer'; -import { SchemaInitializer, SchemaInitializerButtonContext } from '..'; -import { useSchemaTemplateManager } from '../../schema-templates'; -import { useCollectionDataSourceItems } from '../utils'; - -export const FilterBlockInitializer = (props) => { - const { templateWrap, onCreateBlockSchema, componentType, createBlockSchema, insert, items, ...others } = props; - const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const { setVisible } = useContext(SchemaInitializerButtonContext); - const defaultItems = useCollectionDataSourceItems(componentType); - - return ( - } - {...others} - onClick={async ({ item }) => { - if (item.template) { - const s = await getTemplateSchemaByMode(item); - templateWrap ? insert(templateWrap(s, { item })) : insert(s); - } else { - if (onCreateBlockSchema) { - onCreateBlockSchema({ item }); - } else if (createBlockSchema) { - insert(createBlockSchema({ collection: item.name })); - } - } - setVisible(false); - }} - items={items || defaultItems} - /> - ); -}; +export const FilterBlockInitializer = DataBlockInitializer; diff --git a/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx index 615ea08466..8a89581f7d 100644 --- a/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/FilterCollapseBlockInitializer.tsx @@ -1,21 +1,23 @@ import { TableOutlined } from '@ant-design/icons'; import React from 'react'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { createCollapseBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; -export const FilterCollapseBlockInitializer = (props) => { - const { insert, item } = props; - const items = item?.key === 'filterCollapseBlockInTableSelector' && []; +export const FilterCollapseBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + const items = itemConfig?.name === 'filterCollapseBlockInTableSelector' ? [] : undefined; return ( } componentType={'FilterCollapse'} onCreateBlockSchema={async ({ item }) => { const schema = createCollapseBlockSchema({ - collection: item.name, + collection: item.collectionName || item.name, // 与数据区块做区分 blockType: 'filter', }); diff --git a/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx index cafe71fd5f..4de4e8f26e 100644 --- a/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/FilterFormBlockInitializer.tsx @@ -1,21 +1,22 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; +import { useSchemaInitializerItem } from '../../application'; import { createFilterFormBlockSchema } from '../utils'; import { FilterBlockInitializer } from './FilterBlockInitializer'; export const FilterFormBlockInitializer = (props) => { - const { item } = props; - const items = item?.key === 'filterFormBlockInTableSelector' && []; + const itemConfig = useSchemaInitializerItem(); + const items = itemConfig?.name === 'filterFormBlockInTableSelector' ? [] : undefined; return ( } componentType={'FilterFormItem'} templateWrap={(templateSchema, { item }) => { const s = createFilterFormBlockSchema({ template: templateSchema, - collection: item.name, + collection: item.collectionName, }); if (item.template && item.mode === 'reference') { s['x-template-key'] = item.template.key; diff --git a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx index 36a30849b8..901e8984ce 100644 --- a/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/FormBlockInitializer.tsx @@ -2,12 +2,14 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { createFormBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; +import { useSchemaInitializerItem } from '../../application'; -export const FormBlockInitializer = (props) => { - const { isCusomeizeCreate } = props; +export const FormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { isCusomeizeCreate } = itemConfig; return ( } componentType={'FormItem'} templateWrap={(templateSchema, { item }) => { diff --git a/packages/core/client/src/schema-initializer/items/G2PlotInitializer.tsx b/packages/core/client/src/schema-initializer/items/G2PlotInitializer.tsx index 69400e29ca..778aa7a93f 100644 --- a/packages/core/client/src/schema-initializer/items/G2PlotInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/G2PlotInitializer.tsx @@ -1,15 +1,16 @@ import React from 'react'; -import { SchemaInitializer } from '..'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const G2PlotInitializer = (props) => { - const { item, insert, ...others } = props; +export const G2PlotInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); return ( - { insert({ - ...item.schema, + ...itemConfig.schema, }); }} /> diff --git a/packages/core/client/src/schema-initializer/items/GanttBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/GanttBlockInitializer.tsx index 4bb0f1e844..eb249f50b0 100644 --- a/packages/core/client/src/schema-initializer/items/GanttBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/GanttBlockInitializer.tsx @@ -9,17 +9,19 @@ import { useGlobalTheme } from '../../global-theme'; import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; import { createGanttBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const GanttBlockInitializer = (props) => { - const { insert } = props; +export const GanttBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const { getCollectionFields } = useCollectionManager(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); + const itemConfig = useSchemaInitializerItem(); return ( } onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx index f4cfae945e..8971835176 100644 --- a/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/GridCardBlockInitializer.tsx @@ -1,15 +1,17 @@ import React from 'react'; import { OrderedListOutlined } from '@ant-design/icons'; -import { createGridCardBlockSchema, createListBlockSchema } from '../utils'; +import { createGridCardBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; import { useCollectionManager } from '../../collection-manager'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const GridCardBlockInitializer = (props) => { - const { insert } = props; +export const GridCardBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { getCollection } = useCollectionManager(); + const itemConfig = useSchemaInitializerItem(); return ( } componentType={'GridCard'} onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx b/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx index 17878f4040..68a7823a42 100644 --- a/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx +++ b/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx @@ -1,20 +1,20 @@ import { merge } from '@formily/shared'; import React from 'react'; -import { SchemaInitializer } from '..'; import { useCurrentSchema } from '../utils'; +import { SchemaInitializerSwitch, useSchemaInitializer } from '../../application'; export const InitializerWithSwitch = (props) => { - const { type, schema, item, insert, remove: passInRemove, disabled } = props; + const { type, schema, item, remove: passInRemove, disabled } = props; const { exists, remove } = useCurrentSchema( schema?.[type] || item?.schema?.[type], type, item.find, passInRemove ?? item.remove, ); - + const { insert } = useSchemaInitializer(); return ( - { - const { insert } = props; +export const KanbanBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { t } = useTranslation(); const { getCollectionFields } = useCollectionManager(); const options = useContext(SchemaOptionsContext); const api = useAPIClient(); const { theme } = useGlobalTheme(); + const itemConfig = useSchemaInitializerItem(); return ( } onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx index cd499454c4..6602a050d3 100644 --- a/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/ListBlockInitializer.tsx @@ -3,13 +3,15 @@ import { OrderedListOutlined } from '@ant-design/icons'; import { createListBlockSchema } from '../utils'; import { DataBlockInitializer } from './DataBlockInitializer'; import { useCollectionManager } from '../../collection-manager'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const ListBlockInitializer = (props) => { - const { insert } = props; +export const ListBlockInitializer = () => { const { getCollection } = useCollectionManager(); + const { insert } = useSchemaInitializer(); + const itemConfig = useSchemaInitializerItem(); return ( } componentType={'List'} onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx index c90601d7a9..23e714f00a 100644 --- a/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/MarkdownBlockInitializer.tsx @@ -2,14 +2,16 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializer } from '../SchemaInitializer'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const MarkdownBlockInitializer = (props) => { - const { insert } = props; +export const MarkdownBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { t } = useTranslation(); + const itemConfig = useSchemaInitializerItem(); + return ( - } onClick={() => { insert({ diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx index 70861d532c..c3f771ddf1 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationBlockInitializer.tsx @@ -3,18 +3,20 @@ import { TableOutlined } from '@ant-design/icons'; import { useCollectionManager } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager(); - const field = item.field; + const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; return ( - } {...others} onClick={async ({ item }) => { @@ -32,7 +34,7 @@ export const RecordAssociationBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('Table', item, field.target, resource)} + items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx index eeb2aaec6e..2b8f968b18 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationCalendarBlockInitializer.tsx @@ -8,22 +8,24 @@ import { useCollectionManager } from '../../collection-manager'; import { useGlobalTheme } from '../../global-theme'; import { FormDialog, SchemaComponent, SchemaComponentOptions } from '../../schema-component'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createCalendarBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationCalendarBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationCalendarBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { t } = useTranslation(); const options = useContext(SchemaOptionsContext); const { getCollection } = useCollectionManager(); - const field = item.field; + const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; const { theme } = useGlobalTheme(); return ( - } {...others} onClick={async ({ item }) => { @@ -100,7 +102,7 @@ export const RecordAssociationCalendarBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('Calendar', item, field.target, resource)} + items={useRecordCollectionDataSourceItems('Calendar', itemConfig, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx index 0d025112ea..5e66b5d5ee 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx @@ -3,18 +3,20 @@ import { FormOutlined } from '@ant-design/icons'; import { useCollectionManager } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createDetailsBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationDetailsBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationDetailsBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager(); - const field = item.field; + const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; return ( - } {...others} onClick={async ({ item }) => { @@ -32,7 +34,7 @@ export const RecordAssociationDetailsBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('Details', item, field.target, resource)} + items={useRecordCollectionDataSourceItems('Details', itemConfig, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx index 1a76b8d202..ed6bb4d519 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx @@ -2,17 +2,19 @@ import React from 'react'; import { FormOutlined } from '@ant-design/icons'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationFormBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const field = item.field; + const field = itemConfig.field; const collection = field.target; const resource = `${field.collectionName}.${field.name}`; return ( - } {...others} onClick={async ({ item }) => { @@ -55,7 +57,7 @@ export const RecordAssociationFormBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('FormItem', item, collection, resource)} + items={useRecordCollectionDataSourceItems('FormItem', itemConfig, collection, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx index b04d2ce414..e0f7f53beb 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx @@ -3,19 +3,21 @@ import { TableOutlined } from '@ant-design/icons'; import { useCollectionManager } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createGridCardBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationGridCardBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationGridCardBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager(); - const field = item.field; + const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; return ( - } {...others} onClick={async ({ item }) => { @@ -33,7 +35,7 @@ export const RecordAssociationGridCardBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('GridCard', item, field.target, resource)} + items={useRecordCollectionDataSourceItems('GridCard', itemConfig, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx index 9bfe13ecfb..dabb0a2e1d 100644 --- a/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordAssociationListBlockInitializer.tsx @@ -3,19 +3,21 @@ import { TableOutlined } from '@ant-design/icons'; import { useCollectionManager } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createListBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordAssociationListBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordAssociationListBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager(); - const field = item.field; + const field = itemConfig.field; const collection = getCollection(field.target); const resource = `${field.collectionName}.${field.name}`; return ( - } {...others} onClick={async ({ item }) => { @@ -33,7 +35,7 @@ export const RecordAssociationListBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('List', item, field.target, resource)} + items={useRecordCollectionDataSourceItems('List', itemConfig, field.target, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx index ec58f4795c..decfe41730 100644 --- a/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordFormBlockInitializer.tsx @@ -3,17 +3,19 @@ import { FormOutlined } from '@ant-design/icons'; import { useBlockAssociationContext } from '../../block-provider'; import { useCollection } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordFormBlockInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, targetCollection, ...others } = props; +export const RecordFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, targetCollection, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const currentCollection = useCollection(); const collection = targetCollection || currentCollection; const association = useBlockAssociationContext(); return ( - } {...others} onClick={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx index b2b0ff020c..3a92bd8722 100644 --- a/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx @@ -3,21 +3,23 @@ import { FormOutlined } from '@ant-design/icons'; import { useBlockRequestContext } from '../../block-provider'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordReadPrettyAssociationFormBlockInitializer = (props) => { - const { item, onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const RecordReadPrettyAssociationFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const field = item.field; + const field = itemConfig.field; const collection = field.target; const resource = `${field.collectionName}.${field.name}`; const { block } = useBlockRequestContext(); const actionInitializers = block !== 'TableField' ? 'ReadPrettyFormActionInitializers' : null; return ( - } {...others} onClick={async ({ item }) => { @@ -55,7 +57,7 @@ export const RecordReadPrettyAssociationFormBlockInitializer = (props) => { ); } }} - items={useRecordCollectionDataSourceItems('ReadPrettyFormItem', item, collection, resource)} + items={useRecordCollectionDataSourceItems('ReadPrettyFormItem', itemConfig, collection, resource)} /> ); }; diff --git a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx index 761a597591..67889efb95 100644 --- a/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/RecordReadPrettyFormBlockInitializer.tsx @@ -3,32 +3,32 @@ import { FormOutlined } from '@ant-design/icons'; import { useBlockAssociationContext, useBlockRequestContext } from '../../block-provider'; import { useCollection } from '../../collection-manager'; import { useSchemaTemplateManager } from '../../schema-templates'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createReadPrettyFormBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const RecordReadPrettyFormBlockInitializer = (props) => { +export const RecordReadPrettyFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); const { onCreateBlockSchema, componentType, createBlockSchema, - insert, icon = true, targetCollection, ...others - } = props; + } = itemConfig; + const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const currentCollection = useCollection(); const collection = targetCollection || currentCollection; const association = useBlockAssociationContext(); const { block } = useBlockRequestContext(); const actionInitializers = - block !== 'TableField' ? props.actionInitializers || 'ReadPrettyFormActionInitializers' : null; + block !== 'TableField' ? itemConfig.actionInitializers || 'ReadPrettyFormActionInitializers' : null; return ( - } {...others} - key={'123'} onClick={async ({ item }) => { if (item.template) { const s = await getTemplateSchemaByMode(item); diff --git a/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx b/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx index 7424f085ec..a96b90723a 100644 --- a/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/TableActionColumnInitializer.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { InitializerWithSwitch } from './InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../application'; -export const TableActionColumnInitializer = (props) => { +export const TableActionColumnInitializer = () => { const schema = { type: 'void', title: '{{ t("Actions") }}', @@ -23,5 +24,6 @@ export const TableActionColumnInitializer = (props) => { }, }, }; - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx b/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx index bb74cfb151..fc67013810 100644 --- a/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/TableBlockInitializer.tsx @@ -3,13 +3,15 @@ import { TableOutlined } from '@ant-design/icons'; import { useCollectionManager } from '../../collection-manager'; import { DataBlockInitializer } from './DataBlockInitializer'; import { createTableBlockSchema } from '../utils'; +import { useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const TableBlockInitializer = (props) => { - const { insert } = props; +export const TableBlockInitializer = () => { + const { insert } = useSchemaInitializer(); const { getCollection } = useCollectionManager(); + const itemConfig = useSchemaInitializerItem(); return ( } componentType={'Table'} onCreateBlockSchema={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx b/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx index b0c2bb7af5..9bc08ff9ef 100644 --- a/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/TableCollectionFieldInitializer.tsx @@ -2,8 +2,10 @@ import React from 'react'; import { ISchema } from '@formily/react'; import { InitializerWithSwitch } from './InitializerWithSwitch'; +import { useSchemaInitializerItem } from '../../application'; -export const TableCollectionFieldInitializer = (props) => { +export const TableCollectionFieldInitializer = () => { const schema: ISchema = {}; - return ; + const itemConfig = useSchemaInitializerItem(); + return ; }; diff --git a/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx b/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx index 96ccbf061b..9bdcaa5c5c 100644 --- a/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/TableSelectorInitializer.tsx @@ -2,14 +2,16 @@ import { FormOutlined } from '@ant-design/icons'; import React from 'react'; import { useCollection } from '../../collection-manager'; -import { SchemaInitializer } from '../SchemaInitializer'; import { createTableSelectorSchema } from '../utils'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; -export const TableSelectorInitializer = (props) => { - const { onCreateBlockSchema, componentType, createBlockSchema, insert, ...others } = props; +export const TableSelectorInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); const collection = useCollection(); return ( - } {...others} onClick={async ({ item }) => { diff --git a/packages/core/client/src/schema-initializer/types.ts b/packages/core/client/src/schema-initializer/types.ts deleted file mode 100644 index d23bdffd22..0000000000 --- a/packages/core/client/src/schema-initializer/types.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ISchema, Schema } from '@formily/react'; -import { ButtonProps, DropDownProps, MenuItemProps } from 'antd'; - -export interface SchemaInitializerButtonProps extends ButtonProps { - insert?: (s: ISchema) => void; - wrap?: (s: ISchema) => ISchema; - insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; - items?: SchemaInitializerItemOptions[]; - dropdown?: DropDownProps; - component?: any; - designable?: boolean; - onSuccess?: any; -} - -export type SchemaInitializerItemOptions = ItemGroupOptions | SubMenuOptions | ItemOptions | DividerOptions; - -interface ItemCommonOptions { - title?: any; - key?: string; -} - -interface ItemGroupOptions extends ItemCommonOptions { - type: 'itemGroup'; - children?: SchemaInitializerItemOptions[]; - loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[]; -} - -interface SubMenuOptions extends ItemCommonOptions { - type: 'subMenu'; - children?: SchemaInitializerItemOptions[]; - loadChildren?: ({ searchValue }?: { searchValue: string }) => SchemaInitializerItemOptions[]; -} - -type BreakFn = (s: ISchema) => boolean; - -interface RemoveOptions { - removeParentsIfNoChildren?: boolean; - breakRemoveOn?: ISchema | BreakFn; -} - -type RemoveCallback = (s: Schema, options?: RemoveOptions) => void; - -interface ItemOptions extends ItemCommonOptions { - type: 'item'; - component?: any; - schema?: ISchema; - remove?: (schema: Schema, cb: RemoveCallback) => void; - find?: (schema: Schema, key?: string, current?: string) => Schema | null | undefined; - [key: string]: any; -} - -interface DividerOptions { - type: 'divider'; - key?: string; -} - -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; -} diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 4f245b8985..bde76555d1 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -3,7 +3,7 @@ import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react'; import { uid } from '@formily/shared'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaInitializerItemOptions, useFormActiveFields, useFormBlockContext } from '../'; +import { SchemaInitializerItemType, useFormActiveFields, useFormBlockContext } from '../'; import { CollectionFieldOptions, FieldOptions, useCollection, useCollectionManager } from '../collection-manager'; import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useDesignable } from '../schema-component'; @@ -126,8 +126,9 @@ export const useTableColumnInitializerFields = () => { // interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' }); return { type: 'item', + name: field.name, title: field?.uiSchema?.title || field.name, - component: 'TableCollectionFieldInitializer', + Component: 'TableCollectionFieldInitializer', find: findTableColumn, remove: removeTableColumn, schemaInitialize: (s) => { @@ -140,7 +141,7 @@ export const useTableColumnInitializerFields = () => { }, field, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); }; @@ -173,8 +174,9 @@ export const useAssociatedTableColumnInitializerFields = () => { return { type: 'item', + name: subField.name, title: subField?.uiSchema?.title || subField.name, - component: 'TableCollectionFieldInitializer', + Component: 'TableCollectionFieldInitializer', find: findTableColumn, remove: removeTableColumn, schemaInitialize: (s) => { @@ -187,13 +189,14 @@ export const useAssociatedTableColumnInitializerFields = () => { }, field: subField, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); return { type: 'subMenu', + name: field.uiSchema?.title, title: field.uiSchema?.title, children: items, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); return groups; @@ -243,9 +246,10 @@ export const useInheritsTableColumnInitializerFields = () => { }, }; return { + name: k?.uiSchema?.title || k.name, type: 'item', title: k?.uiSchema?.title || k.name, - component: 'TableCollectionFieldInitializer', + Component: 'TableCollectionFieldInitializer', find: findTableColumn, remove: removeTableColumn, schemaInitialize: (s) => { @@ -258,7 +262,7 @@ export const useInheritsTableColumnInitializerFields = () => { }, field: k, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }), }; }); @@ -301,8 +305,9 @@ export const useFormItemInitializerFields = (options?: any) => { // interfaceConfig?.schemaInitialize?.(schema, { field, block: 'Form', readPretty: form.readPretty }); const resultItem = { type: 'item', + name: field.name, title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -314,7 +319,7 @@ export const useFormItemInitializerFields = (options?: any) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; if (block == 'Kanban') { resultItem['find'] = (schema: Schema, key: string, action: string) => { const s = findSchema(schema, 'x-component', block); @@ -367,9 +372,10 @@ export const useFilterFormItemInitializerFields = (options?: any) => { }; } const resultItem = { + name: field?.uiSchema?.title || field.name, type: 'item', title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -381,7 +387,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; return resultItem; }); @@ -427,9 +433,10 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { 'x-collection-field': `${name}.${field.name}.${subField.name}`, }; return { + name: subField?.uiSchema?.title || subField.name, type: 'item', title: subField?.uiSchema?.title || subField.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -440,14 +447,15 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); return { type: 'subMenu', + name: field.uiSchema?.title, title: field.uiSchema?.title, children: items, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); return groups; }; @@ -466,19 +474,17 @@ const getItem = ( return { type: 'subMenu', + name: field.uiSchema?.title, title: field.uiSchema?.title, children: subFields .map((subField) => - // 使用 | 分隔,是为了防止 form.values 中出现 { a: { b: 1 } } 的情况 - // 使用 | 分隔后,form.values 中会出现 { 'a|b': 1 } 的情况,这种情况下 - // 就可以知道该字段是一个关系字段中的输入框,进而特殊处理 getItem(subField, `${schemaName}.${subField.name}`, collectionName, getCollectionFields, [ ...processedCollections, field.target, ]), ) .filter(Boolean), - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; } if (isAssocField(field)) return null; @@ -498,12 +504,13 @@ const getItem = ( }; return { + name: field.uiSchema?.title || field.name, type: 'item', title: field.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }; // 筛选表单相关 @@ -552,9 +559,10 @@ export const useInheritsFormItemInitializerFields = (options?) => { 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { + name: field?.uiSchema?.title || field.name, type: 'item', title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -565,7 +573,7 @@ export const useInheritsFormItemInitializerFields = (options?) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }), }; }); @@ -606,9 +614,10 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { + name: field?.uiSchema?.title || field.name, type: 'item', title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: removeGridFormItem, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -619,7 +628,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }), }; }); @@ -651,9 +660,10 @@ export const useCustomFormItemInitializerFields = (options?: any) => { 'x-collection-field': `${name}.${field.name}`, }; return { + name: field?.uiSchema?.title || field.name, type: 'item', title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: remove, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -664,7 +674,7 @@ export const useCustomFormItemInitializerFields = (options?: any) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }); }; @@ -697,9 +707,10 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { 'x-collection-field': `${name}.${field.name}`, }; return { + name: field?.uiSchema?.title || field.name, type: 'item', title: field?.uiSchema?.title || field.name, - component: 'CollectionFieldInitializer', + Component: 'CollectionFieldInitializer', remove: remove, schemaInitialize: (s) => { interfaceConfig?.schemaInitialize?.(s, { @@ -710,7 +721,7 @@ export const useCustomBulkEditFormItemInitializerFields = (options?: any) => { }); }, schema, - } as SchemaInitializerItemOptions; + } as SchemaInitializerItemType; }), [fields], ); @@ -870,6 +881,25 @@ export const useCollectionDataSourceItems = (componentName) => { ]; }; +export const useCollectionDataSourceItemsV2 = (componentName) => { + const { t } = useTranslation(); + const { collections, getCollectionFields } = useCollectionManager(); + const { getTemplatesByCollection } = useSchemaTemplateManager(); + + const res = useMemo(() => { + return getChildren({ + collections, + getCollectionFields, + componentName, + searchValue: '', + getTemplatesByCollection, + t, + }); + }, [collections, componentName, getCollectionFields, getTemplatesByCollection, t]); + + return res; +}; + export const createDetailsBlockSchema = (options) => { const { formItemInitializers = 'ReadPrettyFormItemInitializers', @@ -1299,7 +1329,7 @@ export const createReadPrettyFormBlockSchema = (options) => { }, }, }; - // console.log(JSON.stringify(schema, null, 2)); + return schema; }; @@ -1558,7 +1588,7 @@ export const createCalendarBlockSchema = (options) => { }, }, }; - console.log(JSON.stringify(schema, null, 2)); + return schema; }; @@ -1688,7 +1718,7 @@ export const createGanttBlockSchema = (options) => { }, }, }; - console.log(JSON.stringify(schema, null, 2)); + return schema; }; export const createKanbanBlockSchema = (options) => { diff --git a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx index b8c90cd89d..425dd1284f 100644 --- a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx +++ b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useCollection, useCollectionManager } from '../collection-manager'; import { useDesignable } from '../schema-component'; import { getTempFieldState } from '../schema-component/antd/form-v2/utils'; -import { SchemaSettings } from '../schema-settings'; +import { SchemaSettingsModalItem, SchemaSettingsSwitchItem } from '../schema-settings'; export const GeneralSchemaItems: React.FC<{ required?: boolean; @@ -23,7 +23,7 @@ export const GeneralSchemaItems: React.FC<{ return ( <> {collectionField && ( - )} - { @@ -75,9 +75,9 @@ export const GeneralSchemaItems: React.FC<{ }); dn.refresh(); }} - > + > {!field.readPretty && ( - )} {field.readPretty && ( - (); + const fieldSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + + return { + title: t('Edit field title'), + schema: { + type: 'object', + title: t('Edit field title'), + properties: { + title: { + title: t('Field title'), + default: field?.title, + description: `${t('Original field title: ')}${collectionField?.uiSchema?.title}`, + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ title }) { + if (title) { + field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }, + }; + }, + useVisible() { + const fieldSchema = useFieldSchema(); + const { getCollectionJoinField } = useCollectionManager(); + const { getField } = useCollection(); + const collectionField = + getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + return !!collectionField; + }, + }, + { + name: 'displayTitle', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Display title'), + checked: fieldSchema['x-decorator-props']?.['showTitle'] ?? true, + onChange(checked) { + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['showTitle'] = checked; + field.decoratorProps.showTitle = checked; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': { + ...fieldSchema['x-decorator-props'], + showTitle: checked, + }, + }, + }); + dn.refresh(); + }, + }; + }, + }, + { + name: 'editDescription', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit description'), + schema: { + type: 'object', + title: t('Edit description'), + properties: { + description: { + // title: t('Description'), + default: field?.description, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ description }) { + field.description = description; + fieldSchema.description = description; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + description: fieldSchema.description, + }, + }); + dn.refresh(); + }, + }; + }, + useVisible() { + const field = useField(); + return !field.readPretty; + }, + }, + { + name: 'editTooltip', + type: 'modal', + useComponentProps() { + const { t } = useTranslation(); + const { dn } = useDesignable(); + const field = useField(); + const fieldSchema = useFieldSchema(); + + return { + title: t('Edit tooltip'), + schema: { + type: 'object', + title: t('Edit tooltip'), + properties: { + tooltip: { + default: fieldSchema?.['x-decorator-props']?.tooltip, + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + 'x-component-props': {}, + }, + }, + } as ISchema, + onSubmit({ tooltip }) { + field.decoratorProps.tooltip = tooltip; + fieldSchema['x-decorator-props'] = fieldSchema['x-decorator-props'] || {}; + fieldSchema['x-decorator-props']['tooltip'] = tooltip; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-decorator-props': fieldSchema['x-decorator-props'], + }, + }); + dn.refresh(); + }, + }; + }, + useVisible() { + const field = useField(); + return field.readPretty; + }, + }, + { + name: 'required', + type: 'switch', + useComponentProps() { + const { t } = useTranslation(); + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn, refresh } = useDesignable(); + + return { + title: t('Required'), + checked: fieldSchema.required as boolean, + onChange(required) { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + field.required = required; + fieldSchema['required'] = required; + schema['required'] = required; + dn.emit('patch', { + schema, + }); + refresh(); + }, + }; + }, + useVisible() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { required = true } = useSchemaToolbar(); + return !field.readPretty && fieldSchema['x-component'] !== 'FormField' && required; + }, + }, +]; diff --git a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx index 388286fa7e..dfccc53bfc 100644 --- a/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx +++ b/packages/core/client/src/schema-items/OpenModeSchemaItems.tsx @@ -2,14 +2,15 @@ import { useField, useFieldSchema } from '@formily/react'; import { Select } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { SchemaInitializerItem, SchemaInitializerSelect } from '../application'; import { useDesignable } from '../schema-component'; -import { SchemaSettings } from '../schema-settings'; +import { SchemaSettingsSelectItem } from '../schema-settings'; interface Options { openMode?: boolean; openSize?: boolean; } -export const OpenModeSchemaItems: React.FC = (options) => { +export const SchemaInitializerOpenModeSchemaItems: React.FC = (options) => { const { openMode = true, openSize = true } = options; const fieldSchema = useFieldSchema(); const field = useField(); @@ -20,7 +21,7 @@ export const OpenModeSchemaItems: React.FC = (options) => { return ( <> {openMode ? ( - = (options) => { /> ) : null} {openSize && ['modal', 'drawer'].includes(openModeValue) ? ( - +
{t('Popup size')}
-
+ ); } diff --git a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts index 9bad5f096b..5beb3fc98a 100644 --- a/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts +++ b/packages/core/client/src/schema-settings/DataTemplates/hooks/useCollectionState.ts @@ -54,6 +54,7 @@ export const useCollectionState = (currentCollectionName: string) => { }; const option = { ...node, + role: 'button', title: React.createElement(TreeNode, node), key: prefix ? `${prefix}.${field.name}` : field.name, isLeaf: true, @@ -103,6 +104,7 @@ export const useCollectionState = (currentCollectionName: string) => { }; const value = prefix ? `${prefix}.${field.name}` : field.name; return { + role: 'button', title: React.createElement(TreeNode, option), key: value, isLeaf: false, @@ -121,6 +123,7 @@ export const useCollectionState = (currentCollectionName: string) => { return data.map((v) => { return { ...v, + role: 'button', title: React.createElement(TreeNode, { ...v, type: v.type }), children: v.children ? parseTreeData(v.children) : null, }; diff --git a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx index b9363c7937..d00c37685f 100644 --- a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx +++ b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx @@ -3,12 +3,14 @@ import { css } from '@emotion/css'; import { useField, useFieldSchema } from '@formily/react'; import { Space } from 'antd'; import classNames from 'classnames'; -import React, { useMemo } from 'react'; +import React, { FC, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { DragHandler, useCompile, useDesignable, useGridContext, useGridRowContext } from '../schema-component'; import { gridRowColWrap } from '../schema-initializer/utils'; -import { SchemaSettings } from './SchemaSettings'; +import { SchemaSettingsDropdown } from './SchemaSettings'; import { useGetAriaLabelOfDesigner } from './hooks/useGetAriaLabelOfDesigner'; +import { SchemaToolbarProvider, useSchemaInitializerRender, useSchemaSettingsRender } from '../application'; +import { useStyles } from './styles'; const titleCss = css` pointer-events: none; @@ -42,21 +44,35 @@ const overrideAntdCSS = css` } `; -export const GeneralSchemaDesigner = (props: any) => { - const { disableInitializer, title, template, draggable = true } = props; +export interface GeneralSchemaDesignerProps { + disableInitializer?: boolean; + title?: string; + template?: any; + schemaSettings?: string; + contextValue?: any; + /** + * @default true + */ + draggable?: boolean; +} + +export const GeneralSchemaDesigner: FC = (props: any) => { + const { disableInitializer, title, template, schemaSettings, contextValue, draggable = true } = props; const { dn, designable } = useDesignable(); const field = useField(); const { t } = useTranslation(); const fieldSchema = useFieldSchema(); const compile = useCompile(); const { getAriaLabel } = useGetAriaLabelOfDesigner(); - const schemaSettingsProps = { dn, field, fieldSchema, }; - + const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender( + fieldSchema['x-settings'] || schemaSettings, + fieldSchema['x-settings-props'], + ); const rowCtx = useGridRowContext(); const ctx = useGridContext(); const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) @@ -66,59 +82,205 @@ export const GeneralSchemaDesigner = (props: any) => { return { insertPosition: 'afterEnd', wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap, - component: ( + Component: (props: any) => ( ), }; - }, [rowCtx?.cols?.length]); + }, [getAriaLabel, rowCtx?.cols?.length]); if (!designable) { return null; } return ( -
- {title && ( -
- - {compile(title)} - {template && ( - - {t('Reference template')}: {templateName || t('Untitled')} - + +
+ {title && ( +
+ + {compile(title)} + {template && ( + + {t('Reference template')}: {templateName || t('Untitled')} + + )} + +
+ )} +
+ + {draggable && ( + + + + )} + {!disableInitializer && + (ctx?.InitializerComponent ? ( + + ) : ( + ctx?.renderSchemaInitializer?.(initializerProps) + ))} + {schemaSettingsExists ? ( + schemaSettingsRender(contextValue) + ) : ( + + } + {...schemaSettingsProps} + > + {props.children} + )}
- )} -
- - {draggable && ( - - - - )} - {!disableInitializer && - (ctx?.InitializerComponent ? ( - - ) : ( - ctx?.renderSchemaInitializer?.(initializerProps) +
+ + ); +}; + +export interface SchemaToolbarProps { + title?: string | string[]; + draggable?: boolean; + initializer?: string | false; + settings?: string | false; + /** + * @default true + */ + showBorder?: boolean; + showBackground?: boolean; +} + +export const SchemaToolbar: FC = (props) => { + const { title, initializer, settings, showBackground, showBorder = true, draggable = true } = props; + const { designable } = useDesignable(); + const fieldSchema = useFieldSchema(); + const compile = useCompile(); + const { getAriaLabel } = useGetAriaLabelOfDesigner(); + + const titleArr = useMemo(() => { + if (!title) return undefined; + if (typeof title === 'string') return [compile(title)]; + if (Array.isArray(title)) return title.map((item) => compile(item)); + }, [compile, title]); + const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender( + fieldSchema['x-settings'] || settings, + fieldSchema['x-settings-props'], + ); + const { render: schemaInitializerRender, exists: schemaInitializerExists } = useSchemaInitializerRender( + fieldSchema['x-initializer'] || initializer, + fieldSchema['x-initializer-props'], + ); + const rowCtx = useGridRowContext(); + const gridContext = useGridContext(); + + const initializerProps: any = useMemo(() => { + return { + insertPosition: 'afterEnd', + wrap: rowCtx?.cols?.length > 1 ? undefined : gridRowColWrap, + Component: (props: any) => ( + + ), + }; + }, [getAriaLabel, rowCtx?.cols?.length]); + + const dragElement = useMemo(() => { + if (draggable === false) return null; + return ( + + + + ); + }, [draggable, getAriaLabel]); + + const initializerElement = useMemo(() => { + if (initializer === false) return null; + if (gridContext?.InitializerComponent || gridContext?.renderSchemaInitializer) { + return gridContext?.InitializerComponent ? ( + + ) : ( + gridContext.renderSchemaInitializer?.(initializerProps) + ); + } + if (!schemaInitializerExists) return null; + return schemaInitializerRender(initializerProps); + }, [gridContext, initializer, initializerProps, schemaInitializerExists, schemaInitializerRender]); + + const settingsElement = useMemo(() => { + return settings !== false && schemaSettingsExists ? schemaSettingsRender() : null; + }, [schemaSettingsExists, schemaSettingsRender, settings]); + const { styles } = useStyles(); + + const toolbarRef = useRef(null); + + useEffect(() => { + const toolbarElement = toolbarRef.current; + + function show() { + if (toolbarElement) { + toolbarElement.style.display = 'block'; + } + } + + function hide() { + if (toolbarElement) { + toolbarElement.style.display = 'none'; + } + } + + if (toolbarElement) { + toolbarElement.parentElement.addEventListener('mouseenter', show); + toolbarElement.parentElement.addEventListener('mouseleave', hide); + } + + return () => { + if (toolbarElement) { + toolbarElement.parentElement.removeEventListener('mouseenter', show); + toolbarElement.parentElement.removeEventListener('mouseleave', hide); + } + }; + }, []); + + if (!designable) { + return null; + } + + return ( +
+ {titleArr && ( +
+ + {titleArr.map((item) => ( + + {item} + ))} - - } - {...schemaSettingsProps} - > - {props.children} - + +
+ )} +
+ + {dragElement} + {initializerElement} + {settingsElement}
diff --git a/packages/core/client/src/schema-settings/LinkageRules/LinkageRuleAction.tsx b/packages/core/client/src/schema-settings/LinkageRules/LinkageRuleAction.tsx index 9189682331..d9b2a8f2cf 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/LinkageRuleAction.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/LinkageRuleAction.tsx @@ -53,6 +53,9 @@ export const FormFieldLinkageRuleAction = observer( `} > { diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 5ed5efb1f4..3985b8bb8e 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -15,12 +15,14 @@ import { MenuItemProps, MenuProps, Modal, + ModalFuncProps, Select, Space, Switch, } from 'antd'; import _, { cloneDeep, get, set } from 'lodash'; import React, { + FC, ReactNode, createContext, useCallback, @@ -62,6 +64,7 @@ import { useGlobalTheme, useLinkageCollectionFilterOptions, useRecord, + useSchemaSettingsItem, useSortFields, } from '..'; import { BlockRequestContext, useFormBlockContext, useFormBlockType, useTableBlockContext } from '../block-provider'; @@ -99,7 +102,7 @@ import { Option } from './VariableInput/type'; import { formatVariableScop } from './VariableInput/utils/formatVariableScop'; import { DataScopeProps } from './types'; -interface SchemaSettingsProps { +export interface SchemaSettingsProps { title?: any; dn?: Designable; field?: GeneralField; @@ -107,7 +110,7 @@ interface SchemaSettingsProps { children?: ReactNode; } -interface SchemaSettingsContextProps { +interface SchemaSettingsContextProps { dn?: Designable; field?: GeneralField; fieldSchema?: Schema; @@ -115,49 +118,15 @@ interface SchemaSettingsContextProps { visible?: any; template?: any; collectionName?: any; + designer?: T; } const SchemaSettingsContext = createContext(null); -export const useSchemaSettings = () => { - return useContext(SchemaSettingsContext); -}; - -interface RemoveProps { - confirm?: any; - removeParentsIfNoChildren?: boolean; - breakRemoveOn?: ISchema | ((s: ISchema) => boolean); +export function useSchemaSettings() { + return useContext(SchemaSettingsContext) as SchemaSettingsContextProps; } -interface ModalItemProps { - title: string; - onSubmit: (values: any) => void; - initialValues?: any; - schema?: ISchema | (() => ISchema); - modalTip?: string; - components?: any; - hidden?: boolean; - scope?: any; - effects?: any; - width?: string | number; - children?: ReactNode; - asyncGetInitialValues?: () => Promise; - eventKey?: string; - hide?: boolean; -} - -type SchemaSettingsNested = { - Remove?: React.FC; - Item?: React.FC; - Divider?: React.FC; - Popup?: React.FC; - SwitchItem?: React.FC; - CascaderItem?: React.FC & Omit & { title: any }>; - DataScope?: React.FC; - ModalItem: React.FC; - [key: string]: any; -}; - interface SchemaSettingsProviderProps { dn?: Designable; field?: GeneralField; @@ -166,6 +135,7 @@ interface SchemaSettingsProviderProps { visible?: any; template?: any; collectionName?: any; + designer?: any; } export const SchemaSettingsProvider: React.FC = (props) => { @@ -180,11 +150,11 @@ export const SchemaSettingsProvider: React.FC = (pr ); }; -export const SchemaSettings: React.FC & SchemaSettingsNested = (props) => { +export const SchemaSettingsDropdown: React.FC = (props) => { const { title, dn, ...others } = props; const [visible, setVisible] = useState(false); const { Component, getMenuItems } = useMenuItem(); - const [isPending, startTransition] = useReactTransition(); + const [, startTransition] = useReactTransition(); const changeMenu = (v: boolean) => { // 当鼠标快速滑过时,终止菜单的渲染,防止卡顿 @@ -195,8 +165,8 @@ export const SchemaSettings: React.FC & SchemaSettingsNeste const items = getMenuItems(() => props.children); - const dropdownMenu = () => ( - <> + return ( + & SchemaSettingsNeste >
{typeof title === 'string' ? {title} : title}
- +
); - - if (dn) { - return ( - - {dropdownMenu()} - - ); - } - return dropdownMenu(); }; -SchemaSettings.Template = function Template(props) { +export const SchemaSettingsTemplate = function Template(props) { const { componentName, collectionName, resourceName, needRender } = props; const { t } = useTranslation(); const { getCollection } = useCollectionManager(); @@ -242,7 +203,7 @@ SchemaSettings.Template = function Template(props) { } if (template) { return ( - { const schema = await copyTemplateSchema(template); @@ -257,11 +218,11 @@ SchemaSettings.Template = function Template(props) { }} > {t('Convert reference to duplicate')} - + ); } return ( - { setVisible(false); @@ -316,7 +277,7 @@ SchemaSettings.Template = function Template(props) { }} > {t('Save as template')} - + ); }; @@ -354,7 +315,7 @@ const findBlockTemplateSchema = (fieldSchema) => { }, null); }; -SchemaSettings.FormItemTemplate = function FormItemTemplate(props) { +export const SchemaSettingsFormItemTemplate = function FormItemTemplate(props) { const { insertAdjacentPosition = 'afterBegin', componentName, collectionName, resourceName } = props; const { t } = useTranslation(); const compile = useCompile(); @@ -369,7 +330,7 @@ SchemaSettings.FormItemTemplate = function FormItemTemplate(props) { } if (template) { return ( - { const schema = await copyTemplateSchema(template); @@ -402,11 +363,11 @@ SchemaSettings.FormItemTemplate = function FormItemTemplate(props) { }} > {t('Convert reference to duplicate')} - + ); } return ( - { setVisible(false); @@ -475,28 +436,24 @@ SchemaSettings.FormItemTemplate = function FormItemTemplate(props) { }} > {t('Save as block template')} - + ); }; -SchemaSettings.Item = function Item(props: { +export interface SchemaSettingsItemProps extends Omit { title: string; - name?: string; - children?: ReactNode; - eventKey?: string; - onClick?: (e: any) => void; -}) { +} +export const SchemaSettingsItem: FC = (props) => { const { pushMenuItem } = useCollectMenuItems(); const { collectMenuItem } = useCollectMenuItem(); - const { eventKey, title, name } = props; + const { eventKey, title } = props; + const { name } = useSchemaSettingsItem(); if (process.env.NODE_ENV !== 'production' && !title) { - throw new Error('SchemaSettings.Item must have a title'); + throw new Error('SchemaSettingsItem must have a title'); } const item = { - role: 'button', - 'aria-label': name || title, key: title, ..._.omit(props, ['children', 'name']), eventKey: eventKey as any, @@ -515,7 +472,11 @@ SchemaSettings.Item = function Item(props: { return null; }; -SchemaSettings.ItemGroup = function ItemGroup(props) { +export interface SchemaSettingsItemGroupProps { + title: string; + children: any[]; +} +export const SchemaSettingsItemGroup: FC = (props) => { const { Component, getMenuItems } = useMenuItem(); const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); @@ -531,13 +492,17 @@ SchemaSettings.ItemGroup = function ItemGroup(props) { return ; }; -SchemaSettings.SubMenu = function SubMenu(props) { +export interface SchemaSettingsSubMenuProps { + title: string; + eventKey?: string; + children: any; +} + +export const SchemaSettingsSubMenu = function SubMenu(props: SchemaSettingsSubMenuProps) { const { Component, getMenuItems } = useMenuItem(); const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { - role: 'button', - 'aria-label': props.title, key, label: props.title, title: props.title, @@ -548,7 +513,7 @@ SchemaSettings.SubMenu = function SubMenu(props) { return ; }; -SchemaSettings.Divider = function Divider() { +export const SchemaSettingsDivider = function Divider() { const { pushMenuItem } = useCollectMenuItems(); const key = useMemo(() => uid(), []); const item = { @@ -560,7 +525,12 @@ SchemaSettings.Divider = function Divider() { return null; }; -SchemaSettings.Remove = function Remove(props: any) { +export interface SchemaSettingsRemoveProps { + confirm?: ModalFuncProps; + removeParentsIfNoChildren?: boolean; + breakRemoveOn?: ISchema | ((s: ISchema) => boolean); +} +export const SchemaSettingsRemove: FC = (props) => { const { confirm, removeParentsIfNoChildren, breakRemoveOn } = props; const { dn, template } = useSchemaSettings(); const { t } = useTranslation(); @@ -572,7 +542,7 @@ SchemaSettings.Remove = function Remove(props: any) { const { removeActiveFieldName } = useFormActiveFields() || {}; return ( - { @@ -606,14 +576,16 @@ SchemaSettings.Remove = function Remove(props: any) { }} > {t('Delete')} - + ); }; -SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: { +interface SchemaSettingsConnectDataBlocksProps { type: FilterBlockType; emptyDescription?: string; -}) { +} + +export const SchemaSettingsConnectDataBlocks: FC = (props) => { const { type, emptyDescription } = props; const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); @@ -654,9 +626,8 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: { }; if (isSameCollection(block.collection, collection)) { return ( - target.uid === block.uid)} onChange={(checked) => { @@ -685,9 +656,8 @@ SchemaSettings.ConnectDataBlocks = function ConnectDataBlocks(props: { const target = targets.find((target) => target.uid === block.uid); // 与筛选区块的数据表具有关系的表 return ( - + {Content.length ? ( Content ) : ( - + - + )} - + ); }; -SchemaSettings.SelectItem = function SelectItem(props) { - const { title, name, options, value, onChange, ...others } = props; +export interface SchemaSettingsSelectItemProps + extends Omit, + Omit { + value?: SelectWithTitleProps['defaultValue']; +} +export const SchemaSettingsSelectItem: FC = (props) => { + const { title, options, value, onChange, ...others } = props; return ( - - - + + + ); }; -SchemaSettings.CascaderItem = (props: CascaderProps & { title: any }) => { +export type SchemaSettingsCascaderItemProps = CascaderProps & Omit & { title: any }; +export const SchemaSettingsCascaderItem: FC = (props) => { const { title, options, value, onChange, ...others } = props; return ( - +
{title} & { title: any }) => { {...props} />
-
+ ); }; -interface SwitchItemProps extends Omit { +export interface SchemaSettingsSwitchItemProps extends Omit { title: string; checked?: boolean; onChange?: (v: boolean) => void; } - -SchemaSettings.SwitchItem = function SwitchItem(props) { - const { title, onChange, name, ...others } = props; +export const SchemaSettingsSwitchItem: FC = (props) => { + const { title, onChange, ...others } = props; const [checked, setChecked] = useState(!!props.checked); return ( - { @@ -800,17 +774,20 @@ SchemaSettings.SwitchItem = function SwitchItem(props) { {title}
- + ); }; -SchemaSettings.PopupItem = function PopupItem(props) { +export interface SchemaSettingsPopupProps extends SchemaSettingsItemProps { + schema?: ISchema; +} +export const SchemaSettingsPopupItem: FC = (props) => { const { schema, ...others } = props; const [visible, setVisible] = useState(false); const ctx = useContext(SchemaSettingsContext); return ( - { @@ -820,7 +797,7 @@ SchemaSettings.PopupItem = function PopupItem(props) { }} > {props.children || props.title} - + { - const { title, onSubmit, initialValues, initialSchema, schema, modalTip, components, scope, ...others } = props; +export interface SchemaSettingsActionModalItemProps + extends SchemaSettingsModalItemProps, + Omit { + uid?: string; + initialSchema?: ISchema; + schema?: ISchema; + beforeOpen?: () => void; + maskClosable?: boolean; +} +export const SchemaSettingsActionModalItem: FC = React.memo((props) => { + const { title, onSubmit, initialValues, beforeOpen, initialSchema, schema, modalTip, components, scope, ...others } = + props; const [visible, setVisible] = useState(false); const [schemaUid, setSchemaUid] = useState(props.uid); const { t } = useTranslation(); @@ -874,8 +861,8 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { await api.resource('uiSchemas').insert({ values: initialSchema }); setSchemaUid(initialSchema['x-uid']); } - if (typeof others?.beforeOpen === 'function') { - others?.beforeOpen?.(); + if (typeof beforeOpen === 'function') { + beforeOpen?.(); } ctx.setVisible(false); setVisible(true); @@ -884,14 +871,14 @@ SchemaSettings.ActionModalItem = React.memo((props: any) => { const onKeyDown = useCallback((e: React.KeyboardEvent): void => e.stopPropagation(), []); return ( <> - {props.children || props.title} - + {createPortal( { ); }); -SchemaSettings.ActionModalItem.displayName = 'SchemaSettings.ActionModalItem'; +SchemaSettingsActionModalItem.displayName = 'SchemaSettingsActionModalItem'; -SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) { +export interface SchemaSettingsModalItemProps { + title: string; + onSubmit: (values: any) => void; + initialValues?: any; + schema?: ISchema | (() => ISchema); + modalTip?: string; + components?: any; + hidden?: boolean; + scope?: any; + effects?: any; + width?: string | number; + children?: ReactNode; + asyncGetInitialValues?: () => Promise; + eventKey?: string; + hide?: boolean; +} +export const SchemaSettingsModalItem: FC = (props) => { const { hidden, title, @@ -954,7 +957,7 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) { return null; } return ( - { @@ -1012,18 +1015,18 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) { }} > {props.children || props.title} - + ); }; -SchemaSettings.BlockTitleItem = function BlockTitleItem() { +export const SchemaSettingsBlockTitleItem = function BlockTitleItem() { const field = useField(); const fieldSchema = useFieldSchema(); const { dn } = useDesignable(); const { t } = useTranslation(); return ( - { }; }; -SchemaSettings.DataTemplates = function DataTemplates(props) { +export const SchemaSettingsDataTemplates = function DataTemplates(props) { const designerCtx = useContext(SchemaComponentContext); const { collectionName } = props; const fieldSchema = useFieldSchema(); @@ -1324,11 +1327,11 @@ SchemaSettings.DataTemplates = function DataTemplates(props) { const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); return ( - + ); }; -SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(props) { +export const SchemaSettingsEnableChildCollections = function EnableChildCollectionsItem(props) { const { collectionName } = props; const fieldSchema = useFieldSchema(); const field = useField(); @@ -1340,7 +1343,7 @@ SchemaSettings.EnableChildCollections = function EnableChildCollectionsItem(prop const collectionField = getCollectionJoinField(fieldSchema?.parent?.['x-collection-field']) || {}; const isAssocationAdd = fieldSchema?.parent?.['x-component'] === 'CollectionField'; return ( - { } }; -SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchema?: Schema }) { +export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: { fieldSchema?: Schema }) { const currentSchema = useFieldSchema(); const fieldSchema = props?.fieldSchema ?? currentSchema; const field: Field = useField(); @@ -1758,7 +1761,7 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem ); return ( - = function DataScopeConfigure(props) { const { t } = useTranslation(); const { getFields } = useCollectionFilterOptionsV2(props.collectionName); const record = useRecord(); @@ -1943,7 +1946,7 @@ SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) { }; return ( - ISchema} @@ -1962,26 +1965,17 @@ export const isPatternDisabled = (fieldSchema: Schema) => { return fieldSchema?.['x-component-props']?.['pattern-disable'] == true; }; -export function SelectWithTitle({ - name, - title, - defaultValue, - onChange, - options, -}: { - name?: string; +interface SelectWithTitleProps { title?: any; defaultValue?: any; options?: any; onChange?: (...args: any[]) => void; -}) { +} +export function SelectWithTitle({ title, defaultValue, onChange, options }: SelectWithTitleProps) { const [open, setOpen] = useState(false); const timerRef = useRef(null); - return (
{ e.stopPropagation(); diff --git a/packages/core/client/src/schema-settings/demos/basic.tsx b/packages/core/client/src/schema-settings/demos/basic.tsx new file mode 100644 index 0000000000..c6354cc553 --- /dev/null +++ b/packages/core/client/src/schema-settings/demos/basic.tsx @@ -0,0 +1,56 @@ +import React, { FC } from 'react'; +import { Application, Plugin, SchemaSettings, SchemaSettingsItem, useSchemaSettingsRender } from '@nocobase/client'; + +const mySchemaSetting = new SchemaSettings({ + name: 'MySchemaSetting', + items: [ + { + name: 'demo1', // 唯一标识 + type: 'item', // 内置类型 + componentProps: { + title: 'DEMO1', + onClick() { + alert('DEMO1'); + }, + }, + }, + { + name: 'demo2', + Component: () => alert('DEMO2')} />, // 直接使用 Component 组件 + }, + ], +}); + +const Root = () => { + const { render } = useSchemaSettingsRender('MySchemaSetting'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + // 注册 schema settings + this.app.schemaSettingsManager.add(mySchemaSetting); + } +} + +class MyPlugin2 extends Plugin { + async load() { + const mySchemaSetting = this.app.schemaSettingsManager.get('MySchemaSetting'); + + // 添加或者修改 schema settings 的 items + const Demo3 = () => alert('DEMO3')} />; + mySchemaSetting.add('demo3', { + Component: Demo3, + }); + + // 移除 demo2 + mySchemaSetting.remove('demo2'); + } +} + +const app = new Application({ + plugins: [MyPlugin, MyPlugin2], + providers: [Root], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-settings/demos/built-type.tsx b/packages/core/client/src/schema-settings/demos/built-type.tsx new file mode 100644 index 0000000000..28e0062527 --- /dev/null +++ b/packages/core/client/src/schema-settings/demos/built-type.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { Input } from 'antd'; +import { + Application, + FormItem, + SchemaComponentOptions, + SchemaComponentPlugin, + SchemaSettings, + useSchemaSettingsRender, +} from '@nocobase/client'; + +const mySchemaSetting = new SchemaSettings({ + name: 'MySchemaSetting', + items: [ + { + name: 'demo1', // 唯一标识 + type: 'item', // 文本类型 + componentProps: { + title: 'Text', + onClick() { + alert('Text'); + }, + }, + }, + { + name: 'demo2', + type: 'subMenu', // 子菜单 + componentProps: { + title: 'Sub Menu', + }, + children: [ + { + name: 'demo3', + type: 'switch', // Switch + componentProps: { + title: 'Switch1', + }, + }, + { + name: 'demo4', + type: 'switch', + componentProps: { + title: 'Switch2', + }, + }, + ], + }, + { + name: 'demo5', + type: 'divider', // 分割线 + }, + { + name: 'demo6', + type: 'itemGroup', // 分组 + componentProps: { + title: 'Group', + }, + children: [ + { + name: 'demo7', + type: 'select', // Switch + componentProps: { + title: 'Select1', + options: [ + { + label: 'a', + value: 'a', + }, + { + label: 'b', + value: 'b', + }, + ], + }, + }, + { + name: 'demo8', + type: 'cascader', // 级联 + componentProps: { + title: 'Cascader', + options: [ + { + label: 'zhejiang', + value: 'Zhejiang', + children: [ + { + value: 'hangzhou', + label: 'Hangzhou', + }, + ], + }, + ], + }, + }, + ], + }, + { + name: 'demo9', + type: 'modal', + componentProps: { + title: 'Modal', + schema: { + type: 'object', + title: 'Edit button', + properties: { + title: { + 'x-decorator': 'FormItem', + 'x-component': 'Input', + title: 'Button title', + default: 'aaa', + 'x-component-props': {}, + }, + }, + }, + onSubmit() { + alert(123); + }, + }, + }, + ], +}); + +const DemoRoot = () => { + const { render } = useSchemaSettingsRender('MySchemaSetting'); + return ( + +
{render()}
+
+ ); +}; + +const app = new Application({ + schemaSettings: [mySchemaSetting], + providers: [DemoRoot], + plugins: [SchemaComponentPlugin], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-settings/demos/custom-component.tsx b/packages/core/client/src/schema-settings/demos/custom-component.tsx new file mode 100644 index 0000000000..c5ab148c01 --- /dev/null +++ b/packages/core/client/src/schema-settings/demos/custom-component.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Application, Plugin, SchemaSettings, useSchemaSettingsRender } from '@nocobase/client'; +import { Button } from 'antd'; + +const mySchemaSetting = new SchemaSettings({ + name: 'MySchemaSetting', + Component: Button, // 自定义组件 + componentProps: { + type: 'primary', + children: '自定义按钮', + }, + // Component: () => , // 等同于上面效果 + items: [ + { + name: 'demo1', + type: 'item', + componentProps: { + title: 'DEMO', + }, + }, + ], +}); + +const Root = () => { + const { render } = useSchemaSettingsRender('MySchemaSetting'); + return
{render()}
; +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.add(mySchemaSetting); + } +} + +const app = new Application({ + plugins: [MyPlugin], + providers: [Root], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-settings/demos/demo3.tsx b/packages/core/client/src/schema-settings/demos/demo3.tsx new file mode 100644 index 0000000000..4a6d52d531 --- /dev/null +++ b/packages/core/client/src/schema-settings/demos/demo3.tsx @@ -0,0 +1,110 @@ +import { + Application, + CardItem, + Grid, + Plugin, + SchemaComponent, + SchemaInitializer, + SchemaInitializerItem, + SchemaSettings, + useSchemaComponentContext, + useSchemaInitializer, + useSchemaInitializerItem, +} from '@nocobase/client'; +import { Button } from 'antd'; +import React from 'react'; + +const mySettings = new SchemaSettings({ + name: 'mySettings', + items: [ + { + name: 'remove', + type: 'remove', + componentProps: { + removeParentsIfNoChildren: true, + }, + }, + ], +}); + +const myInitializer = new SchemaInitializer({ + name: 'MyInitializer', + // 按钮标题标题 + title: 'Button Text', + wrap: Grid.wrap, + // 调用 initializer.render() 时会渲染 items 列表 + items: [ + { + name: 'demo1', + title: 'Demo1', + Component: () => { + const itemConfig = useSchemaInitializerItem(); + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const handleClick = () => { + insert({ + type: 'void', + 'x-settings': 'mySettings', + 'x-decorator': 'CardItem', + 'x-component': 'Hello', + }); + }; + return ; + }, + }, + ], +}); + +const Hello = () =>

Hello, world!

; + +const Btn = () => { + const { designable, setDesignable } = useSchemaComponentContext(); + return ( + + ); +}; + +const HelloPage = () => { + return ( +
+ + +
+ ); +}; + +class PluginHello extends Plugin { + async load() { + this.app.addComponents({ Grid, CardItem, Hello }); + this.app.schemaSettingsManager.add(mySettings); + this.app.schemaInitializerManager.add(myInitializer); + this.router.add('hello', { + path: '/', + Component: HelloPage, + }); + } +} + +const app = new Application({ + router: { + type: 'memory', + }, + designable: true, + plugins: [PluginHello], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-settings/demos/schema-basic.tsx b/packages/core/client/src/schema-settings/demos/schema-basic.tsx new file mode 100644 index 0000000000..8ee0eea3a7 --- /dev/null +++ b/packages/core/client/src/schema-settings/demos/schema-basic.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { + Application, + Plugin, + SchemaComponent, + SchemaComponentProvider, + SchemaSettings, + useSchemaSettingsRender, +} from '@nocobase/client'; +import { observer, useFieldSchema } from '@formily/react'; + +const mySchemaSetting = new SchemaSettings({ + name: 'MySchemaSetting', + items: [ + { + name: 'demo1', + type: 'item', + componentProps: { + title: 'DEMO', + }, + }, + ], +}); + +const DemoDesigner = () => { + const filedSchema = useFieldSchema(); + // 从 schema 中读取 name + const { exists, render } = useSchemaSettingsRender(filedSchema['x-settings'], filedSchema['x-settings-props']); + + return
{exists && render()}
; +}; + +const Page = observer( + (props) => { + return ( +
+ {props.children} + +
+ ); + }, + { displayName: 'Page' }, +); + +const Root = () => { + return ( + + + + ); +}; + +class MyPlugin extends Plugin { + async load() { + this.app.schemaSettingsManager.add(mySchemaSetting); + } +} + +const app = new Application({ + plugins: [MyPlugin], + providers: [Root], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts index 00e8ad3eeb..09ab38ce25 100644 --- a/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts +++ b/packages/core/client/src/schema-settings/hooks/useGetAriaLabelOfDesigner.ts @@ -8,19 +8,20 @@ import { useCollection } from '../../collection-manager'; */ export const useGetAriaLabelOfDesigner = () => { const fieldSchema = useFieldSchema(); - let { name: collectionName } = useCollection(); - const component = fieldSchema['x-component']; - const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : ''; - const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : ''; - collectionName = collectionName ? `-${collectionName}` : ''; - + const { name: _collectionName } = useCollection(); const getAriaLabel = useCallback( (name: string, postfix?: string) => { + if (!fieldSchema) return ''; + + const component = fieldSchema['x-component']; + const designer = fieldSchema['x-designer'] ? `-${fieldSchema['x-designer']}` : ''; + const collectionField = fieldSchema['x-collection-field'] ? `-${fieldSchema['x-collection-field']}` : ''; + const collectionName = _collectionName ? `-${_collectionName}` : ''; postfix = postfix ? `-${postfix}` : ''; return `designer-${name}-${component}${designer}${collectionName}${collectionField}${postfix}`; }, - [collectionField, collectionName, component, designer], + [fieldSchema, _collectionName], ); return { getAriaLabel }; diff --git a/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts b/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts index 4656ecf97b..ca649a6bec 100644 --- a/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts +++ b/packages/core/client/src/schema-settings/hooks/useIsShowMultipleSwitch.ts @@ -14,8 +14,8 @@ export function useIsShowMultipleSwitch() { : null; const uiSchema = collectionField?.uiSchema || fieldSchema; const hasMultiple = uiSchema['x-component-props']?.multiple === true; - const fieldMode=field?.componentProps?.['mode']; + const fieldMode = field?.componentProps?.['mode']; return function IsShowMultipleSwitch() { - return !field.readPretty && fieldSchema['x-component'] !== 'TableField' && hasMultiple&&fieldMode!=='SubTable'; + return !field.readPretty && fieldSchema['x-component'] !== 'TableField' && hasMultiple && fieldMode !== 'SubTable'; }; } diff --git a/packages/core/client/src/schema-settings/index.md b/packages/core/client/src/schema-settings/index.md index a7700ee3c1..534612df2e 100644 --- a/packages/core/client/src/schema-settings/index.md +++ b/packages/core/client/src/schema-settings/index.md @@ -1 +1,515 @@ -# SchemaSettings \ No newline at end of file +--- +group: + title: Client + order: 1 +--- + +# SchemaSettings + +## API + +### new SchemaSettings(options) + +创建一个 Schema 配置实例。 + +#### 参数类型 + +```ts | pure +interface SchemaSettingOptions{ + name: string; + Component?: ComponentType; + componentProps?: T; + style?: React.CSSProperties; + items: SchemaSettingsItemType[]; +} +``` + +#### 参数详细信息 + +- name + +用于标识 Schema 配置的名称。 + +会用在 schema 中的 `x-settings` 配置值以及读取 schema 的值会传给 [useSchemaSettingRender()](#useschemasettingsrender) 的第一个参数。 + +```ts | pure +const mySettings = new SchemaSettings({ + // 定义 name + name: 'MySettings', +}) +``` + +- Component、componentProps & style + +Component 默认是一个 Icon,如果需要定制化,可以为一个 React 组件。 + +```tsx | pure +import { SettingOutlined } from '@ant-design/icons'; +const mySettings = new SchemaSettings({ + Component: () => +}) +``` + +如果你使用的是一个公共的组件,不同的 Settings 有定制化的诉求,那么你可以使用 componentProps 和 style。 + +```tsx | pure +import { Button } from '@ant-design/icons'; +const mySettings = new SchemaSettings({ + Component: Button, + componentProps: { + type: 'primary' + } +}) +``` + +当然也可以这样是使用。 + +```tsx | pure +const mySettings = new SchemaSettings({ + Component: () =>
setEditingTitle(ev.target.value)} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/FormBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/FormBlockInitializer.tsx index 1a4bece06b..12627ef1ac 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/FormBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/FormBlockInitializer.tsx @@ -2,10 +2,12 @@ import React from 'react'; import { CollectionProvider, - SchemaInitializer, - SchemaInitializerItemOptions, + SchemaInitializerItem, + SchemaInitializerItemType, createFormBlockSchema, useRecordCollectionDataSourceItems, + useSchemaInitializer, + useSchemaInitializerItem, useSchemaTemplateManager, } from '@nocobase/client'; @@ -14,9 +16,10 @@ import { traverseSchema } from './utils'; import { JOB_STATUS } from '../../constants'; import { NAMESPACE } from '../../locale'; -function InternalFormBlockInitializer({ insert, schema, ...others }) { +function InternalFormBlockInitializer({ schema, ...others }) { const { getTemplateSchemaByMode } = useSchemaTemplateManager(); - const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemOptions[]; + const { insert } = useSchemaInitializer(); + const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[]; async function onConfirm({ item }) { const template = item.template ? await getTemplateSchemaByMode(item) : null; const result = createFormBlockSchema({ @@ -57,13 +60,14 @@ function InternalFormBlockInitializer({ insert, schema, ...others }) { insert(result); } - return ; + return ; } -export function FormBlockInitializer(props) { +export function FormBlockInitializer() { + const itemConfig = useSchemaInitializerItem(); return ( - - + + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/SchemaConfig.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/SchemaConfig.tsx index 5622db6cba..03818c98e0 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/SchemaConfig.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/SchemaConfig.tsx @@ -15,9 +15,12 @@ import { SchemaComponent, SchemaComponentContext, SchemaInitializer, - SchemaInitializerItemOptions, - SchemaInitializerProvider, - SchemaSettings, + SchemaInitializerItem, + SchemaInitializerItemType, + SchemaSettingsBlockTitleItem, + SchemaSettingsDivider, + SchemaSettingsItem, + SchemaSettingsRemove, VariableScopeProvider, css, gridRowColWrap, @@ -25,6 +28,8 @@ import { useCompile, useFormActiveFields, useFormBlockContext, + useSchemaInitializer, + useSchemaInitializerItem, useSchemaOptionsContext, } from '@nocobase/client'; import { Registry, lodash } from '@nocobase/utils/client'; @@ -58,7 +63,7 @@ export type FormType = { export type ManualFormType = { title: string; config: { - useInitializer: ({ collections }?: { collections: any[] }) => SchemaInitializerItemOptions; + useInitializer: ({ collections }?: { collections: any[] }) => SchemaInitializerItemType; initializers?: { [key: string]: React.FC; }; @@ -83,7 +88,7 @@ manualFormTypes.register('customForm', customRecordForm); manualFormTypes.register('createForm', createRecordForm); manualFormTypes.register('updateForm', updateRecordForm); -function useTriggerInitializers(): SchemaInitializerItemOptions | null { +function useTriggerInitializers(): SchemaInitializerItemType | null { const { workflow } = useFlowContext(); const trigger = useTrigger(); return trigger.useInitializers ? trigger.useInitializers(workflow.config) : null; @@ -100,9 +105,9 @@ function SimpleDesigner() { const compile = useCompile(); return ( - - - + + { - const instruction = instructions.get(node.type); - return instruction?.useInitializers?.(node); - }) - .filter(Boolean); - const dataBlockInitializers = [ - ...triggerInitializers, - ...(nodeBlockInitializers.length - ? [ - { - key: 'nodes', - type: 'subMenu', - title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, - children: nodeBlockInitializers, - }, - ] - : []), - ].filter(Boolean); - - const items = [ - ...(dataBlockInitializers.length - ? [ - { - type: 'itemGroup', - title: '{{t("Data blocks")}}', - children: dataBlockInitializers, - }, - ] - : []), +export const addBlockButton = new SchemaInitializer({ + name: 'AddBlockButton', + wrap: gridRowColWrap, + title: '{{t("Add block")}}', + items: [ { type: 'itemGroup', - title: '{{t("Form")}}', - children: Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => { - const { useInitializer: getInitializer } = item.config; - return getInitializer({ collections }); - }), + name: 'dataBlocks', + title: '{{t("Data blocks")}}', + checkChildrenLength: true, + useChildren() { + const current = useNodeContext(); + const nodes = useAvailableUpstreams(current); + const triggerInitializers = [useTriggerInitializers()].filter(Boolean); + const nodeBlockInitializers = nodes + .map((node) => { + const instruction = instructions.get(node.type); + return instruction?.useInitializers?.(node); + }) + .filter(Boolean); + const dataBlockInitializers: any = [ + ...triggerInitializers, + ...(nodeBlockInitializers.length + ? [ + { + name: 'nodes', + type: 'subMenu', + title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, + children: nodeBlockInitializers, + }, + ] + : []), + ].filter(Boolean); + return dataBlockInitializers; + }, }, { type: 'itemGroup', + name: 'form', + title: '{{t("Form")}}', + useChildren() { + const { collections } = useCollectionManager(); + return Array.from(manualFormTypes.getValues()).map((item: ManualFormType) => { + const { useInitializer: getInitializer } = item.config; + return getInitializer({ collections }); + }); + }, + }, + { + type: 'itemGroup', + name: 'otherBlocks', title: '{{t("Other blocks")}}', children: [ { - type: 'item', + name: 'markdown', title: '{{t("Markdown")}}', - component: 'MarkdownBlockInitializer', + Component: 'MarkdownBlockInitializer', }, ], }, - ] as SchemaInitializerItemOptions[]; - - return ; -} + ], +}); function AssignedFieldValues() { const ctx = useContext(SchemaComponentContext); @@ -228,9 +237,9 @@ function AssignedFieldValues() { return ( <> - setOpen(true)}> + setOpen(true)}> {title} - + - - + { insert({ type: 'void', - title: props.title, + title: others.title, 'x-decorator': 'ManualActionStatusProvider', 'x-decorator-props': { value: action, @@ -319,13 +331,16 @@ function ContinueInitializer({ action, actionProps, insert, ...props }) { ); } -function ActionInitializer({ action, actionProps, ...props }) { +function ActionInitializer() { + const itemConfig = useSchemaInitializerItem(); + const { action, actionProps, ...others } = itemConfig; return ( - ); -} +export const addActionButton = new SchemaInitializer({ + name: 'AddActionButton', + title: '{{t("Configure actions")}}', + items: [ + { + name: 'jobStatusResolved', + title: `{{t("Continue the process", { ns: "${NAMESPACE}" })}}`, + Component: ContinueInitializer, + action: JOB_STATUS.RESOLVED, + actionProps: { + type: 'primary', + }, + }, + { + name: 'jobStatusRejected', + title: `{{t("Terminate the process", { ns: "${NAMESPACE}" })}}`, + Component: ActionInitializer, + action: JOB_STATUS.REJECTED, + actionProps: { + danger: true, + }, + }, + { + name: 'jobStatusPending', + title: `{{t("Save temporarily", { ns: "${NAMESPACE}" })}}`, + Component: ActionInitializer, + action: JOB_STATUS.PENDING, + }, + ], +}); // NOTE: fake useAction for ui configuration function useSubmit() { @@ -392,7 +400,6 @@ function useSubmit() { export function SchemaConfig({ value, onChange }) { const ctx = useContext(SchemaComponentContext); - const trigger = useTrigger(); const node = useNodeContext(); const nodes = useAvailableUpstreams(node); const form = useForm(); @@ -478,46 +485,32 @@ export function SchemaConfig({ value, onChange }) { refresh, }} > - Object.assign(result, item.config.initializers), + (result, item: ManualFormType) => Object.assign(result, item.config.components), {}, ), + FormBlockProvider, + DetailsBlockProvider, + // NOTE: fake provider component + ManualActionStatusProvider(props) { + return props.children; + }, + ActionBarProvider(props) { + return props.children; + }, + SimpleDesigner, + ManualActionDesigner, }} - > - Object.assign(result, item.config.components), - {}, - ), - FormBlockProvider, - DetailsBlockProvider, - // NOTE: fake provider component - ManualActionStatusProvider(props) { - return props.children; - }, - ActionBarProvider(props) { - return props.children; - }, - SimpleDesigner, - ManualActionDesigner, - }} - scope={{ - useSubmit, - useDetailsBlockProps: useFormBlockContext, - }} - /> - + scope={{ + useSubmit, + useDetailsBlockProps: useFormBlockContext, + }} + /> ); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/WorkflowTodoBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/WorkflowTodoBlockInitializer.tsx index 09fa47cc83..be28c92b02 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/WorkflowTodoBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/WorkflowTodoBlockInitializer.tsx @@ -1,13 +1,15 @@ import React, { FC } from 'react'; import { TableOutlined } from '@ant-design/icons'; -import { SchemaInitializer, useCollectionManager } from '@nocobase/client'; +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '@nocobase/client'; -export const WorkflowTodoBlockInitializer: FC = ({ insert, ...rest }) => { +export const WorkflowTodoBlockInitializer: FC = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); return ( - } - {...rest} + {...itemConfig} onClick={() => { insert({ type: 'void', diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/create.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/create.tsx index 2cc033bee7..e1fde959ac 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/create.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/create.tsx @@ -1,7 +1,17 @@ -import React from 'react'; +import React, { useMemo, useState } from 'react'; -import { GeneralSchemaDesigner, SchemaSettings, useCollection } from '@nocobase/client'; +import { + GeneralSchemaDesigner, + SchemaSettingsBlockTitleItem, + SchemaSettingsDataTemplates, + SchemaSettingsDivider, + SchemaSettingsLinkageRules, + SchemaSettingsRemove, + useCollection, + useMenuSearch, +} from '@nocobase/client'; +import _ from 'lodash'; import { NAMESPACE } from '../../../locale'; import { FormBlockInitializer } from '../FormBlockInitializer'; import { ManualFormType } from '../SchemaConfig'; @@ -12,11 +22,11 @@ function CreateFormDesigner() { return ( - - - - - + + + + + collections.map((item) => ({ + name: _.camelCase(`createRecordForm-child-${item.name}`), + type: 'item', + title: item.title, + label: item.label, + schema: { + collection: item.name, + title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`, + formType: 'create', + 'x-designer': 'CreateFormDesigner', + }, + Component: FormBlockInitializer, + })), + [collections], + ); + const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); + const searchedChildren = useMenuSearch(childItems, isOpenSubMenu, true); return { + name: 'createRecordForm', key: 'createRecordForm', type: 'subMenu', title: `{{t("Create record form", { ns: "${NAMESPACE}" })}}`, - children: [ - { - key: 'createRecordForm-child', - type: 'itemGroup', - style: { - maxHeight: '48vh', - overflowY: 'auto', - }, - loadChildren: ({ searchValue }) => { - return collections - .filter((item) => !item.hidden && item.title.toLowerCase().includes(searchValue.toLowerCase())) - .map((item) => ({ - key: `createRecordForm-child-${item.name}`, - type: 'item', - title: item.title, - schema: { - collection: item.name, - title: `{{t("Create record", { ns: "${NAMESPACE}" })}}`, - formType: 'create', - 'x-designer': 'CreateFormDesigner', - }, - component: FormBlockInitializer, - })); - }, + componentProps: { + onOpenChange(keys) { + setIsOpenSubMenu(keys.length > 0); }, - ], - }; + }, + children: searchedChildren, + } as any; }, initializers: { // AddCustomFormField diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/custom.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/custom.tsx index ca1751ff91..ef3e52f75f 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/custom.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/custom.tsx @@ -12,10 +12,14 @@ import { RecordProvider, SchemaComponent, SchemaInitializer, - SchemaInitializerItemOptions, + SchemaInitializerItem, + SchemaInitializerItemType, + SchemaInitializerItems, gridRowColWrap, useCollectionManager, useRecord, + useSchemaInitializer, + useSchemaInitializerItem, } from '@nocobase/client'; import { merge, uid } from '@nocobase/utils/client'; import lodash from 'lodash'; @@ -62,10 +66,12 @@ function CustomFormBlockProvider(props) { ) : null; } -function CustomFormBlockInitializer({ insert, ...props }) { +function CustomFormBlockInitializer() { + const { insert } = useSchemaInitializer(); + const itemConfig = useSchemaInitializerItem(); return ( - { insert({ type: 'void', @@ -175,19 +181,19 @@ function getOptions(interfaces) { })); } -function useCommonInterfaceInitializers(): SchemaInitializerItemOptions[] { +function useCommonInterfaceInitializers(): SchemaInitializerItemType[] { const { interfaces } = useCollectionManager(); const options = getOptions(interfaces); return options.map((group) => ({ - key: group.title, + name: group.title, type: 'itemGroup', title: group.title, children: group.children.map((item) => ({ - key: item.name, + name: item.name, type: 'item', title: item.title, - component: CustomFormFieldInitializer, + Component: CustomFormFieldInitializer, fieldInterface: item.name, })), })); @@ -195,12 +201,11 @@ function useCommonInterfaceInitializers(): SchemaInitializerItemOptions[] { const AddCustomFormFieldButtonContext = React.createContext({}); -function AddCustomFormField(props) { - const { insertPosition = 'beforeEnd', component } = props; - const items = useCommonInterfaceInitializers(); - const collection = useContext(CollectionContext); +const CustomItemsComponent = (props) => { const [interfaceOptions, setInterface] = useState(null); const [insert, setCallback] = useState(); + const items = useCommonInterfaceInitializers(); + const collection = useContext(CollectionContext); const { setCollectionFields } = useContext(FormBlockContext); return ( @@ -220,13 +225,7 @@ function AddCustomFormField(props) { setCallback, }} > - + {interfaceOptions ? ( ); -} +}; -function CustomFormFieldInitializer(props) { - const { item, insert } = props; +export const addCustomFormField = new SchemaInitializer({ + name: 'AddCustomFormField', + wrap: gridRowColWrap, + insertPosition: 'beforeEnd', + title: "{{t('Configure fields')}}", + ItemsComponent: CustomItemsComponent, +}); + +function CustomFormFieldInitializer() { + const itemConfig = useSchemaInitializerItem(); + const { insert, setVisible } = useSchemaInitializer(); const { onAddField, setCallback } = useContext(AddCustomFormFieldButtonContext); const { getInterface } = useCollectionManager(); - const interfaceOptions = getInterface(item.fieldInterface); + const interfaceOptions = getInterface(itemConfig.fieldInterface); return ( - { setCallback(() => insert); onAddField(interfaceOptions); + setVisible(false); }} + {...itemConfig} /> ); } @@ -342,15 +352,13 @@ export default { config: { useInitializer() { return { - key: 'customForm', + name: 'customForm', type: 'item', title: `{{t("Custom form", { ns: "${NAMESPACE}" })}}`, - component: CustomFormBlockInitializer, + Component: CustomFormBlockInitializer, }; }, - initializers: { - AddCustomFormField, - }, + initializers: {}, components: { CustomFormBlockProvider, }, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/update.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/update.tsx index 838c08cece..a380d1f5a8 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/update.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/forms/update.tsx @@ -1,15 +1,21 @@ import { useFieldSchema } from '@formily/react'; -import React from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { GeneralSchemaDesigner, - SchemaSettings, + SchemaSettingsActionModalItem, + SchemaSettingsBlockTitleItem, + SchemaSettingsDivider, + SchemaSettingsLinkageRules, + SchemaSettingsRemove, useCollection, useCollectionFilterOptions, useDesignable, + useMenuSearch, } from '@nocobase/client'; +import _ from 'lodash'; import { FilterDynamicComponent } from '../../../components/FilterDynamicComponent'; import { NAMESPACE } from '../../../locale'; import { FormBlockInitializer } from '../FormBlockInitializer'; @@ -24,8 +30,8 @@ function UpdateFormDesigner() { return ( - - + - - - + + + collections.map((item) => ({ + name: _.camelCase(`updateRecordForm-child-${item.name}`), + type: 'item', + title: item.title, + label: item.label, + schema: { + collection: item.name, + title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`, + formType: 'update', + 'x-designer': 'UpdateFormDesigner', + }, + Component: FormBlockInitializer, + })), + [collections], + ); + const [isOpenSubMenu, setIsOpenSubMenu] = useState(false); + const searchedChildren = useMenuSearch(childItems, isOpenSubMenu, true); return { + name: 'updateRecordForm', key: 'updateRecordForm', type: 'subMenu', title: `{{t("Update record form", { ns: "${NAMESPACE}" })}}`, - children: [ - { - key: 'updateRecordForm-child', - type: 'itemGroup', - style: { - maxHeight: '48vh', - overflowY: 'auto', - }, - loadChildren: ({ searchValue }) => { - return collections - .filter((item) => !item.hidden && item.title.toLowerCase().includes(searchValue.toLowerCase())) - .map((item) => ({ - key: `updateRecordForm-child-${item.name}`, - type: 'item', - title: item.title, - schema: { - collection: item.name, - title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`, - formType: 'update', - 'x-designer': 'UpdateFormDesigner', - }, - component: FormBlockInitializer, - })); - }, + componentProps: { + onOpenChange(keys) { + setIsOpenSubMenu(keys.length > 0); }, - ], - }; + }, + children: searchedChildren, + } as any; }, initializers: { // AddCustomFormField diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx index 445da407e1..d06411ad8d 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/manual/index.tsx @@ -1,11 +1,11 @@ -import { BlockInitializers, SchemaInitializerItemOptions, useCollectionManager, useCompile } from '@nocobase/client'; +import { SchemaInitializerItemType, useCollectionManager, useCompile } from '@nocobase/client'; import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer'; -import { defaultFieldNames, getCollectionFieldOptions } from '../../variable'; import { NAMESPACE } from '../../locale'; -import { SchemaConfig, SchemaConfigButton } from './SchemaConfig'; -import { ModeConfig } from './ModeConfig'; +import { defaultFieldNames, getCollectionFieldOptions } from '../../variable'; import { AssigneesSelect } from './AssigneesSelect'; +import { ModeConfig } from './ModeConfig'; +import { SchemaConfig, SchemaConfigButton } from './SchemaConfig'; const MULTIPLE_ASSIGNED_MODE = { SINGLE: Symbol('single'), @@ -15,18 +15,6 @@ const MULTIPLE_ASSIGNED_MODE = { ANY_PERCENTAGE: Symbol('any percentage'), }; -// TODO(optimize): change to register way -const initializerGroup = BlockInitializers.items.find((group) => group.key === 'media'); -if (!initializerGroup.children.find((item) => item.key === 'workflowTodos')) { - initializerGroup.children.push({ - key: 'workflowTodos', - type: 'item', - title: `{{t("Workflow todos", { ns: "${NAMESPACE}" })}}`, - component: 'WorkflowTodoBlockInitializer', - icon: 'CheckSquareOutlined', - } as any); -} - export default { title: `{{t("Manual", { ns: "${NAMESPACE}" })}}`, type: 'manual', @@ -125,7 +113,7 @@ export default { } : null; }, - useInitializers(node): SchemaInitializerItemOptions | null { + useInitializers(node): SchemaInitializerItemType | null { const { getCollection } = useCollectionManager(); const formKeys = Object.keys(node.config.forms ?? {}); if (!formKeys.length || node.config.mode) { @@ -139,18 +127,20 @@ export default { return fields.length ? ({ + name: form.title ?? formKey, type: 'item', title: form.title ?? formKey, - component: CollectionBlockInitializer, + Component: CollectionBlockInitializer, collection: form.collection, dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`, - } as SchemaInitializerItemOptions) + } as SchemaInitializerItemType) : null; }) .filter(Boolean); return forms.length ? { + name: 'forms', key: 'forms', type: 'subMenu', title: node.title, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx index c2ac2f354a..de481e49a1 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/query.tsx @@ -2,18 +2,18 @@ import { ArrayItems } from '@formily/antd-v5'; import { SchemaComponentContext, - SchemaInitializerItemOptions, + SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile, } from '@nocobase/client'; -import { appends, collection, filter, pagination, sort } from '../schemas/collection'; -import { NAMESPACE } from '../locale'; +import { useForm } from '@formily/react'; import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer'; import { FilterDynamicComponent } from '../components/FilterDynamicComponent'; +import { NAMESPACE } from '../locale'; +import { appends, collection, filter, pagination, sort } from '../schemas/collection'; import { WorkflowVariableInput, getCollectionFieldOptions } from '../variable'; -import { useForm } from '@formily/react'; export default { title: `{{t("Query record", { ns: "${NAMESPACE}" })}}`, @@ -117,15 +117,16 @@ export default { return result; }, - useInitializers(node): SchemaInitializerItemOptions | null { + useInitializers(node): SchemaInitializerItemType | null { if (!node.config.collection || node.config.multiple) { return null; } return { + name: node.title ?? `#${node.id}`, type: 'item', title: node.title ?? `#${node.id}`, - component: CollectionBlockInitializer, + Component: CollectionBlockInitializer, collection: node.config.collection, dataSource: `{{$jobsMapByNodeKey.${node.key}}}`, }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx index be380adc9d..6e3da9a091 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/collection.tsx @@ -1,9 +1,4 @@ -import { - SchemaInitializerItemOptions, - useCollectionDataSource, - useCollectionManager, - useCompile, -} from '@nocobase/client'; +import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer'; import { FieldsSelect } from '../components/FieldsSelect'; import { NAMESPACE, lang } from '../locale'; @@ -178,16 +173,17 @@ export default { }); return result; }, - useInitializers(config): SchemaInitializerItemOptions | null { + useInitializers(config): SchemaInitializerItemType | null { if (!config.collection) { return null; } return { + name: 'triggerData', type: 'item', key: 'triggerData', title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`, - component: CollectionBlockInitializer, + Component: CollectionBlockInitializer, collection: config.collection, dataSource: '{{$context.data}}', }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/form.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/form.tsx index 2ba09abb65..97a2abd78c 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/form.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/form.tsx @@ -1,6 +1,6 @@ import { useField, useFieldSchema, useForm } from '@formily/react'; import { - SchemaInitializerItemOptions, + SchemaInitializerItemType, useAPIClient, useActionContext, useBlockRequestContext, @@ -82,16 +82,17 @@ export default { }); return result; }, - useInitializers(config): SchemaInitializerItemOptions | null { + useInitializers(config): SchemaInitializerItemType | null { if (!config.collection) { return null; } return { + name: 'triggerData', type: 'item', key: 'triggerData', title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`, - component: CollectionBlockInitializer, + Component: CollectionBlockInitializer, collection: config.collection, dataSource: '{{$context.data}}', }; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx index df1544dd43..06e835bf64 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx @@ -5,7 +5,7 @@ import { ActionContextProvider, FormProvider, SchemaComponent, - SchemaInitializerItemOptions, + SchemaInitializerItemType, css, cx, useAPIClient, @@ -15,6 +15,7 @@ import { } from '@nocobase/client'; import { Registry } from '@nocobase/utils/client'; import { Button, Input, Tag, message } from 'antd'; +import { cloneDeep } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useFlowContext } from '../FlowContext'; import { DrawerDescription } from '../components/DrawerDescription'; @@ -24,7 +25,6 @@ import { VariableOptions } from '../variable'; import collection from './collection'; import formTrigger from './form'; import schedule from './schedule/'; -import { cloneDeep } from 'lodash'; function useUpdateConfigAction() { const form = useForm(); @@ -62,7 +62,7 @@ export interface Trigger { view?: ISchema; scope?: { [key: string]: any }; components?: { [key: string]: any }; - useInitializers?(config): SchemaInitializerItemOptions | null; + useInitializers?(config): SchemaInitializerItemType | null; initializers?: any; useActionTriggerable?: boolean | (() => boolean); } @@ -233,8 +233,6 @@ export const TriggerConfig = () => {
setEditingTitle(ev.target.value)} onBlur={(ev) => onChangeTitle(ev.target.value)} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx index 7e55937dda..b142d52ecc 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/schedule/index.tsx @@ -1,9 +1,4 @@ -import { - SchemaInitializerItemOptions, - useCollectionDataSource, - useCollectionManager, - useCompile, -} from '@nocobase/client'; +import { SchemaInitializerItemType, useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client'; import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer'; import { NAMESPACE, lang } from '../../locale'; @@ -65,15 +60,16 @@ export default { } return options; }, - useInitializers(config): SchemaInitializerItemOptions | null { + useInitializers(config): SchemaInitializerItemType | null { if (!config.collection) { return null; } return { + name: 'triggerData', type: 'item', title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`, - component: CollectionBlockInitializer, + Component: CollectionBlockInitializer, collection: config.collection, dataSource: '{{$context.data}}', }; diff --git a/playwright.config.ts b/playwright.config.ts index 6d938c8a55..28154da8a7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - timeout: process.env.CI ? 60 * 1000 : 30 * 1000, + timeout: process.env.CI ? 2 * 60 * 1000 : 30 * 1000, // Look for test files in the "tests" directory, relative to this configuration file. testDir: 'packages', diff --git a/scripts/runE2e.setup.ts b/scripts/runE2e.setup.ts index b19c021b8b..a319d4d05b 100644 --- a/scripts/runE2e.setup.ts +++ b/scripts/runE2e.setup.ts @@ -8,10 +8,13 @@ process.on('SIGINT', () => { }); const run = async () => { - const { awaitForNocoBase } = await runNocoBase({ - stdio: 'ignore', // 不输出服务的日志,避免干扰测试的日志 - signal: abortController.signal, - }); + const { awaitForNocoBase } = await runNocoBase( + { + stdio: 'ignore', // 不输出服务的日志,避免干扰测试的日志 + signal: abortController.signal, + }, + true, + ); await awaitForNocoBase(); diff --git a/scripts/utils.ts b/scripts/utils.ts index 2ef033fa88..2e5c5a028f 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -112,6 +112,7 @@ export const runNocoBase = async ( force?: boolean; signal?: AbortSignal; }, + clearDatabase = false, ) => { // 用于存放 playwright 自动生成的相关的文件 if (!fs.existsSync('playwright')) { @@ -155,9 +156,11 @@ export const runNocoBase = async ( return { awaitForNocoBase }; } - // 加上 -f 会清空数据库 - console.log('yarn nocobase install -f'); - await runCommand('yarn', ['nocobase', 'install', '-f'], options); + if (clearDatabase) { + // 加上 -f 会清空数据库 + console.log('yarn nocobase install -f'); + await runCommand('yarn', ['nocobase', 'install', '-f'], options); + } if (await checkPort(PORT)) { console.log('Server is running, skip starting server.'); diff --git a/yarn.lock b/yarn.lock index 2813ad69d4..8cea7f4dda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6905,6 +6905,15 @@ "@vitest/utils" "0.34.3" chai "^4.3.7" +"@vitest/expect@0.34.6": + version "0.34.6" + resolved "https://registry.npmmirror.com/@vitest/expect/-/expect-0.34.6.tgz#608a7b7a9aa3de0919db99b4cc087340a03ea77e" + integrity sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw== + dependencies: + "@vitest/spy" "0.34.6" + "@vitest/utils" "0.34.6" + chai "^4.3.10" + "@vitest/runner@0.34.3": version "0.34.3" resolved "https://registry.npmmirror.com/@vitest/runner/-/runner-0.34.3.tgz#ce09b777d133bbcf843e1a67f4a743365764e097" @@ -6913,6 +6922,15 @@ p-limit "^4.0.0" pathe "^1.1.1" +"@vitest/runner@0.34.6": + version "0.34.6" + resolved "https://registry.npmmirror.com/@vitest/runner/-/runner-0.34.6.tgz#6f43ca241fc96b2edf230db58bcde5b974b8dcaf" + integrity sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ== + dependencies: + "@vitest/utils" "0.34.6" + p-limit "^4.0.0" + pathe "^1.1.1" + "@vitest/snapshot@0.34.3": version "0.34.3" resolved "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-0.34.3.tgz#cb4767aa44711a1072bd2e06204b659275c4f0f2" @@ -6921,12 +6939,28 @@ pathe "^1.1.1" pretty-format "^29.5.0" +"@vitest/snapshot@0.34.6": + version "0.34.6" + resolved "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-0.34.6.tgz#b4528cf683b60a3e8071cacbcb97d18b9d5e1d8b" + integrity sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w== + dependencies: + magic-string "^0.30.1" + pathe "^1.1.1" + pretty-format "^29.5.0" + "@vitest/spy@0.34.3": version "0.34.3" resolved "https://registry.npmmirror.com/@vitest/spy/-/spy-0.34.3.tgz#d4cf25e6ca9230991a0223ecd4ec2df30f0784ff" dependencies: tinyspy "^2.1.1" +"@vitest/spy@0.34.6": + version "0.34.6" + resolved "https://registry.npmmirror.com/@vitest/spy/-/spy-0.34.6.tgz#b5e8642a84aad12896c915bce9b3cc8cdaf821df" + integrity sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ== + dependencies: + tinyspy "^2.1.1" + "@vitest/utils@0.34.3": version "0.34.3" resolved "https://registry.npmmirror.com/@vitest/utils/-/utils-0.34.3.tgz#6e243189a358b736b9fc0216e6b6979bc857e897" @@ -6935,6 +6969,15 @@ loupe "^2.3.6" pretty-format "^29.5.0" +"@vitest/utils@0.34.6": + version "0.34.6" + resolved "https://registry.npmmirror.com/@vitest/utils/-/utils-0.34.6.tgz#38a0a7eedddb8e7291af09a2409cb8a189516968" + integrity sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A== + dependencies: + diff-sequences "^29.4.3" + loupe "^2.3.6" + pretty-format "^29.5.0" + "@xmldom/xmldom@0.8.7": version "0.8.7" resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" @@ -8561,6 +8604,19 @@ cfb@^1.1.4: adler-32 "~1.3.0" crc-32 "~1.2.0" +chai@^4.3.10: + version "4.3.10" + resolved "https://registry.npmmirror.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" + integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + chai@^4.3.7: version "4.3.7" resolved "https://registry.npmmirror.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" @@ -8661,6 +8717,13 @@ check-error@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.npmmirror.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + china-division@^2.4.0: version "2.6.1" resolved "https://registry.npmmirror.com/china-division/-/china-division-2.6.1.tgz#a3e3e4609e81077cc97443f78c713e03d66d7fc2" @@ -10264,9 +10327,10 @@ dedupe@^3.0.2: version "3.0.3" resolved "https://registry.npmmirror.com/dedupe/-/dedupe-3.0.3.tgz#7ae7b55ca01028bc7d5714cd57a5bdf5e4aeea6e" -deep-eql@^4.1.2: +deep-eql@^4.1.2, deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== dependencies: type-detect "^4.0.0" @@ -10713,9 +10777,10 @@ dumi-assets-types@2.0.0-alpha.0: version "2.0.0-alpha.0" resolved "https://registry.npmmirror.com/dumi-assets-types/-/dumi-assets-types-2.0.0-alpha.0.tgz#46bf619ed1cb6d27bbe6a9cfe4be51e5e9589981" -dumi-theme-nocobase@^0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/dumi-theme-nocobase/-/dumi-theme-nocobase-0.2.14.tgz#4cfcc786754c7d813c205245cbfbe3dfa76cc1b3" +dumi-theme-nocobase@^0.2.18: + version "0.2.18" + resolved "https://registry.yarnpkg.com/dumi-theme-nocobase/-/dumi-theme-nocobase-0.2.18.tgz#91f9462ad738f347c1e6fb4e304170a891aa8804" + integrity sha512-M+FP/gL9yRazibyBXPUV4XRGaJb+MT+o4VU+oMja1RAWcCvzTYNeNQsXifpAvVnUdaGwNlESyJCU9hlLzB8ARQ== dependencies: "@ant-design/icons" "^5.1.3" "@babel/runtime" "^7.22.3" @@ -12478,6 +12543,11 @@ get-func-name@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" +get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.1" resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" @@ -22788,7 +22858,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.npmmirror.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -23517,6 +23587,18 @@ vite-node@0.34.3: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" +vite-node@0.34.6: + version "0.34.6" + resolved "https://registry.npmmirror.com/vite-node/-/vite-node-0.34.6.tgz#34d19795de1498562bf21541a58edcd106328a17" + integrity sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + mlly "^1.4.0" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" + vite-plugin-css-injected-by-js@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.2.1.tgz#c23e10e28a1afb78414fa3c162ac8a253cd1a6a4" @@ -23548,6 +23630,17 @@ vite@4.3.1: optionalDependencies: fsevents "~2.3.2" +"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0": + version "4.5.0" + resolved "https://registry.npmmirror.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" + integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + vite@^4.4.9: version "4.4.9" resolved "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d" @@ -23559,7 +23652,7 @@ vite@^4.4.9: optionalDependencies: fsevents "~2.3.2" -vitest@0.x, vitest@^0.34.3: +vitest@0.x: version "0.34.3" resolved "https://registry.npmmirror.com/vitest/-/vitest-0.34.3.tgz#863d61c133d01b16e49fd52d380c09fa5ac03188" dependencies: @@ -23588,6 +23681,36 @@ vitest@0.x, vitest@^0.34.3: vite-node "0.34.3" why-is-node-running "^2.2.2" +vitest@^0.34.6: + version "0.34.6" + resolved "https://registry.npmmirror.com/vitest/-/vitest-0.34.6.tgz#44880feeeef493c04b7f795ed268f24a543250d7" + integrity sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q== + dependencies: + "@types/chai" "^4.3.5" + "@types/chai-subset" "^1.3.3" + "@types/node" "*" + "@vitest/expect" "0.34.6" + "@vitest/runner" "0.34.6" + "@vitest/snapshot" "0.34.6" + "@vitest/spy" "0.34.6" + "@vitest/utils" "0.34.6" + acorn "^8.9.0" + acorn-walk "^8.2.0" + cac "^6.7.14" + chai "^4.3.10" + debug "^4.3.4" + local-pkg "^0.4.3" + magic-string "^0.30.1" + pathe "^1.1.1" + picocolors "^1.0.0" + std-env "^3.3.3" + strip-literal "^1.0.1" + tinybench "^2.5.0" + tinypool "^0.7.0" + vite "^3.1.0 || ^4.0.0 || ^5.0.0-0" + vite-node "0.34.6" + why-is-node-running "^2.2.2" + vizion@~2.2.1: version "2.2.1" resolved "https://registry.npmmirror.com/vizion/-/vizion-2.2.1.tgz#04201ea45ffd145d5b5210e385a8f35170387fb2"