From acafe0f386f645349245b1f8dfad4d26678c88e2 Mon Sep 17 00:00:00 2001 From: jack zhang <1098626505@qq.com> Date: Tue, 20 Jun 2023 17:11:18 +0800 Subject: [PATCH 01/26] fix: mobile docs style (#2083) --- .dumirc.ts | 4 ++-- docs/config.ts | 12 +++--------- docs/en-US/api/cli.md | 2 +- package.json | 2 +- yarn.lock | 8 ++++---- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.dumirc.ts b/.dumirc.ts index 71453f5951..eeb0d14d71 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -46,8 +46,8 @@ export default defineConfig({ github: 'https://github.com/nocobase/nocobase', footer: 'nocobase | Copyright © 2022', localesEnhance: [ - { id: 'zh-CN', switchPrefix: '中' }, - { id: 'en-US', switchPrefix: 'en' } + { id: 'zh-CN', switchPrefix: '中', hostname: 'docs-cn.nocobase.com' }, + { id: 'en-US', switchPrefix: 'en', hostname: 'docs.nocobase.com' } ], }), // mfsu: true, // 报错 diff --git a/docs/config.ts b/docs/config.ts index 47527f5b57..c0867ad3b4 100644 --- a/docs/config.ts +++ b/docs/config.ts @@ -285,23 +285,17 @@ const sidebar = { }, ], }, - '/api/cli', - '/api/actions', - '/api/sdk', { title: '@nocobase/cli', - path: '/api/cli', - type: 'item', + link: '/api/cli', }, { title: '@nocobase/actions', - path: '/api/actions', - type: 'item', + link: '/api/actions', }, { title: '@nocobase/sdk', - path: '/api/sdk', - type: 'item', + link: '/api/sdk', }, ], }; diff --git a/docs/en-US/api/cli.md b/docs/en-US/api/cli.md index 39be52aef4..a96084f02d 100644 --- a/docs/en-US/api/cli.md +++ b/docs/en-US/api/cli.md @@ -1,4 +1,4 @@ -# NocoBase CLI +# @nocobase/cli The NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications. diff --git a/package.json b/package.json index e4a812d9ea..c466d1af17 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@vitejs/plugin-react": "^4.0.0", "auto-changelog": "^2.4.0", "dumi": "^2.2.0", - "dumi-theme-nocobase": "^0.2.11", + "dumi-theme-nocobase": "^0.2.12", "ghooks": "^2.0.4", "jsdom-worker": "^0.3.0", "prettier": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index 875f1c0e43..12c11d8625 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11727,10 +11727,10 @@ dumi-assets-types@2.0.0-alpha.0: resolved "https://registry.yarnpkg.com/dumi-assets-types/-/dumi-assets-types-2.0.0-alpha.0.tgz#46bf619ed1cb6d27bbe6a9cfe4be51e5e9589981" integrity sha512-a/Y5lf0G6gwsEQ9hop/n03CcjmHsGBk384Cz/AEX6mRYrfSpUx/lQvP9HLoXkCzScl9PL1sSmLPnMkgaXDCZLA== -dumi-theme-nocobase@^0.2.11: - version "0.2.11" - resolved "https://registry.npmjs.org/dumi-theme-nocobase/-/dumi-theme-nocobase-0.2.11.tgz#690c64cd284c913d7f6428bc291c3a6cdf0f2d50" - integrity sha512-hpMJPgIYeUIXKlfxYI1LD3KL4vr6ZlFzeSXNVqAurnlLmP5zkyiGekA/Azgg5zSkeWdaYa+fbB6CPAVE9RCZ+Q== +dumi-theme-nocobase@^0.2.12: + version "0.2.12" + resolved "https://registry.yarnpkg.com/dumi-theme-nocobase/-/dumi-theme-nocobase-0.2.12.tgz#663c9bab60ae6dcfd5255b99780b4ddb7942ace7" + integrity sha512-ObjJZkKWYqeZ+JlQGOX71vI+FPD9RxASYRrFZV9vpkIALwkLfolwX+8JZTHq9VS8dG3+3tAukVY25FOagVs4ZA== dependencies: "@ant-design/icons" "^5.1.3" "@babel/runtime" "^7.22.3" From 5672ffc9fa4b7b311e97fbb59fce8c368369c9c7 Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 20 Jun 2023 18:05:06 +0800 Subject: [PATCH 02/26] feat: update docs --- deploy-docs-cn.sh | 9 +++++++++ deploy-docs.sh | 9 +++++++++ .../welcome/getting-started/upgrading/git-clone.md | 10 +++++----- docs/en-US/welcome/release/v10-changelog.md | 2 +- .../welcome/getting-started/upgrading/git-clone.md | 4 ++-- 5 files changed, 26 insertions(+), 8 deletions(-) create mode 100755 deploy-docs-cn.sh create mode 100755 deploy-docs.sh diff --git a/deploy-docs-cn.sh b/deploy-docs-cn.sh new file mode 100755 index 0000000000..66a9021728 --- /dev/null +++ b/deploy-docs-cn.sh @@ -0,0 +1,9 @@ +cd docs/dist/zh-CN +echo "docs-cn.nocobase.com" >> CNAME +echo "" >> .nojekyll +git init +git remote add origin git@github.com:nocobase/docs-cn.nocobase.com.git +git branch -M main +git add . +git commit -m "first commit" +git push -f origin main diff --git a/deploy-docs.sh b/deploy-docs.sh new file mode 100755 index 0000000000..913028eabe --- /dev/null +++ b/deploy-docs.sh @@ -0,0 +1,9 @@ +cd docs/dist/en-US +echo "docs.nocobase.com" >> CNAME +echo "" >> .nojekyll +git init +git remote add origin git@github.com:nocobase/docs.nocobase.com.git +git branch -M main +git add . +git commit -m "first commit" +git push -f origin main diff --git a/docs/en-US/welcome/getting-started/upgrading/git-clone.md b/docs/en-US/welcome/getting-started/upgrading/git-clone.md index 3eb0aa0279..732f8a1116 100644 --- a/docs/en-US/welcome/getting-started/upgrading/git-clone.md +++ b/docs/en-US/welcome/getting-started/upgrading/git-clone.md @@ -17,11 +17,11 @@ git pull v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉以下目录之后再升级 ```bash -# 删除 .umi 相关缓存 -yarn rimraf -rf ./**/{.umi,.umi-production} -# 删除编译文件 -yarn rimraf -rf packages/*/*/{lib,esm,es,dist,node_modules} -# 删除全部依赖 +# Remove .umi cache +yarn rimraf -rf "./**/{.umi,.umi-production}" +# Delete compiled files +yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" +# Remove dependencies yarn rimraf -rf node_modules ``` diff --git a/docs/en-US/welcome/release/v10-changelog.md b/docs/en-US/welcome/release/v10-changelog.md index 2dbbcebb15..c05f3f1da7 100644 --- a/docs/en-US/welcome/release/v10-changelog.md +++ b/docs/en-US/welcome/release/v10-changelog.md @@ -44,7 +44,7 @@ No change, upgrade reference [Upgrading for Docker compose](/welcome/getting-sta v0.10 has a major upgrade of dependencies, so to prevent errors when upgrading the source code, you need to delete the following directories before upgrading ```bash -### Remove .umi-related cache +# Remove .umi cache yarn rimraf -rf "./**/{.umi,.umi-production}" # Delete compiled files yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" diff --git a/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md b/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md index 093a15ea66..e1dd0258cf 100644 --- a/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md +++ b/docs/zh-CN/welcome/getting-started/upgrading/git-clone.md @@ -18,9 +18,9 @@ v0.10 进行了依赖的重大升级,如果 v0.9 升级 v0.10,需要删掉 ```bash # 删除 .umi 相关缓存 -yarn rimraf -rf ./**/{.umi,.umi-production} +yarn rimraf -rf "./**/{.umi,.umi-production}" # 删除编译文件 -yarn rimraf -rf packages/*/*/{lib,esm,es,dist,node_modules} +yarn rimraf -rf "./packages/*/*/{lib,esm,es,dist,node_modules}" # 删除全部依赖 yarn rimraf -rf node_modules ``` From 56e6d0c3b1ca0fe77184bbee724485065de848da Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 21 Jun 2023 09:53:31 +0700 Subject: [PATCH 03/26] fix(plugin-workflow): fix occasional error on enter workflow page (#2086) --- packages/plugins/workflow/src/client/triggers/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/plugins/workflow/src/client/triggers/index.tsx b/packages/plugins/workflow/src/client/triggers/index.tsx index 1a2caf618b..1bee28ebbd 100644 --- a/packages/plugins/workflow/src/client/triggers/index.tsx +++ b/packages/plugins/workflow/src/client/triggers/index.tsx @@ -141,6 +141,7 @@ export const TriggerConfig = () => { const { workflow, refresh } = useFlowContext(); const [editingTitle, setEditingTitle] = useState(''); const [editingConfig, setEditingConfig] = useState(false); + let typeTitle = ''; useEffect(() => { if (workflow) { setEditingTitle(workflow.title ?? typeTitle); @@ -151,7 +152,9 @@ export const TriggerConfig = () => { return null; } const { title, type, config, executed } = workflow; - const { title: typeTitle, fieldset, scope, components } = triggers.get(type); + const trigger = triggers.get(type); + const { fieldset, scope, components } = trigger; + typeTitle = trigger.title; const detailText = executed ? '{{t("View")}}' : '{{t("Configure")}}'; const titleText = `${lang('Trigger')}: ${compile(typeTitle)}`; From 64070b81b9a388fe1bcc3adb585cdaa59495f244 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 21 Jun 2023 11:02:49 +0800 Subject: [PATCH 04/26] refactor: migrate adminSchemaUid & mobileSchemaUid to system settings (#2084) * refactor: migrate adminSchemaUid & mobileSchemaUid to system settings * fix: error --- .../route-switch/antd/admin-layout/index.tsx | 12 +++++- .../20230620203213-admin-ui-schema-uid.ts | 18 ++++++++ packages/plugins/client/src/server.ts | 29 +++++++++++++ .../src/client/router/Application.tsx | 24 ++++++++--- .../20230620203215-mobile-ui-schema-uid.ts | 18 ++++++++ .../mobile-client/src/server/plugin.ts | 42 ++++++++++++++++++- 6 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 packages/plugins/client/src/migrations/20230620203213-admin-ui-schema-uid.ts create mode 100644 packages/plugins/mobile-client/src/server/migrations/20230620203215-mobile-ui-schema-uid.ts diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index c43a56b982..8dee2a0563 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -54,6 +54,12 @@ const useMenuProps = () => { defaultSelectedUid, }; }; + +const useAdminSchemaUid = () => { + const ctx = useSystemSettings(); + return ctx?.data?.data?.options?.adminSchemaUid; +}; + const MenuEditor = (props) => { const { setTitle } = useDocumentTitle(); const navigate = useNavigate(); @@ -70,12 +76,14 @@ const MenuEditor = (props) => { navigate(`/admin/${schema['x-uid']}`); }; + const adminSchemaUid = useAdminSchemaUid(); + const { data, loading } = useRequest( { - url: `/uiSchemas:getJsonSchema/${route.uiSchemaUid}`, + url: `/uiSchemas:getJsonSchema/${adminSchemaUid}`, }, { - refreshDeps: [route.uiSchemaUid], + refreshDeps: [adminSchemaUid], onSuccess(data) { const schema = filterByACL(data?.data, ctx); // url 为 `/admin` 的情况 diff --git a/packages/plugins/client/src/migrations/20230620203213-admin-ui-schema-uid.ts b/packages/plugins/client/src/migrations/20230620203213-admin-ui-schema-uid.ts new file mode 100644 index 0000000000..a956d793eb --- /dev/null +++ b/packages/plugins/client/src/migrations/20230620203213-admin-ui-schema-uid.ts @@ -0,0 +1,18 @@ +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + async up() { + const systemSettings = this.db.getRepository('systemSettings'); + const instance = await systemSettings.findOne(); + const uiRoutes = this.db.getRepository('uiRoutes'); + const routes = await uiRoutes.find(); + for (const route of routes) { + if (route.uiSchemaUid && route?.options?.component === 'AdminLayout') { + instance.set('options.adminSchemaUid', route.uiSchemaUid); + console.log('options.adminSchemaUid', route.uiSchemaUid); + await instance.save(); + return; + } + } + } +} diff --git a/packages/plugins/client/src/server.ts b/packages/plugins/client/src/server.ts index b407bb7876..8370c6655f 100644 --- a/packages/plugins/client/src/server.ts +++ b/packages/plugins/client/src/server.ts @@ -87,9 +87,38 @@ export class ClientPlugin extends Plugin { // } }); + + this.db.on('systemSettings.beforeCreate', async (instance, { transaction }) => { + const uiSchemas = this.db.getRepository('uiSchemas'); + const schema = await uiSchemas.insert( + { + type: 'void', + 'x-component': 'Menu', + 'x-designer': 'Menu.Designer', + 'x-initializer': 'MenuItemInitializers', + 'x-component-props': { + mode: 'mix', + theme: 'dark', + // defaultSelectedUid: 'u8', + onSelect: '{{ onSelect }}', + sideMenuRefScopeKey: 'sideMenuRef', + }, + properties: {}, + }, + { transaction }, + ); + instance.set('options.adminSchemaUid', schema['x-uid']); + }); } async load() { + this.db.addMigrations({ + namespace: 'client', + directory: resolve(__dirname, './migrations'), + context: { + plugin: this, + }, + }); this.app.acl.allow('app', 'getLang'); this.app.acl.allow('app', 'getInfo'); this.app.acl.allow('app', 'getPlugins'); diff --git a/packages/plugins/mobile-client/src/client/router/Application.tsx b/packages/plugins/mobile-client/src/client/router/Application.tsx index b4bc48d0dc..cb622fcc57 100644 --- a/packages/plugins/mobile-client/src/client/router/Application.tsx +++ b/packages/plugins/mobile-client/src/client/router/Application.tsx @@ -1,10 +1,17 @@ +import { css, cx } from '@emotion/css'; +import { + ActionContextProvider, + AdminProvider, + RemoteSchemaComponent, + useRoute, + useSystemSettings, + useViewport, +} from '@nocobase/client'; +import { DrawerProps, ModalProps } from 'antd'; import React, { useMemo } from 'react'; import { Outlet, useParams } from 'react-router-dom'; -import { ActionContextProvider, AdminProvider, RemoteSchemaComponent, useRoute, useViewport } from '@nocobase/client'; -import { css, cx } from '@emotion/css'; -import { useInterfaceContext } from './InterfaceProvider'; -import { DrawerProps, ModalProps } from 'antd'; import { MobileCore } from '../core'; +import { useInterfaceContext } from './InterfaceProvider'; const commonCSSVariables = css` --nb-spacing: 14px; @@ -66,8 +73,15 @@ const modalProps = { `, }; +const useMobileSchemaUid = () => { + const ctx = useSystemSettings(); + return ctx?.data?.data?.options?.mobileSchemaUid; +}; + const MApplication: React.FC = (props) => { const route = useRoute(); + const mobileSchemaUid = useMobileSchemaUid(); + console.log('mobileSchemaUid', mobileSchemaUid); const params = useParams<{ name: string }>(); const interfaceContext = useInterfaceContext(); const Provider = useMemo(() => { @@ -99,7 +113,7 @@ const MApplication: React.FC = (props) => { {params.name && !params.name.startsWith('tab_') ? ( ) : ( - + {props.children} )} diff --git a/packages/plugins/mobile-client/src/server/migrations/20230620203215-mobile-ui-schema-uid.ts b/packages/plugins/mobile-client/src/server/migrations/20230620203215-mobile-ui-schema-uid.ts new file mode 100644 index 0000000000..a86662d8e0 --- /dev/null +++ b/packages/plugins/mobile-client/src/server/migrations/20230620203215-mobile-ui-schema-uid.ts @@ -0,0 +1,18 @@ +import { Migration } from '@nocobase/server'; + +export default class extends Migration { + async up() { + const systemSettings = this.db.getRepository('systemSettings'); + const instance = await systemSettings.findOne(); + const uiRoutes = this.db.getRepository('uiRoutes'); + const routes = await uiRoutes.find(); + for (const route of routes) { + if (route.uiSchemaUid && route?.options?.component === 'MApplication') { + instance.set('options.mobileSchemaUid', route.uiSchemaUid); + console.log('options.mobileSchemaUid', route.uiSchemaUid); + await instance.save(); + return; + } + } + } +} diff --git a/packages/plugins/mobile-client/src/server/plugin.ts b/packages/plugins/mobile-client/src/server/plugin.ts index b6e93e2d16..30bd5a114d 100644 --- a/packages/plugins/mobile-client/src/server/plugin.ts +++ b/packages/plugins/mobile-client/src/server/plugin.ts @@ -1,10 +1,19 @@ -import { InstallOptions, Plugin } from '@nocobase/server'; +import { Plugin } from '@nocobase/server'; +import { resolve } from 'path'; import { routes } from './routes'; export class MobileClientPlugin extends Plugin { afterAdd() {} - async load() {} + async load() { + this.db.addMigrations({ + namespace: 'client', + directory: resolve(__dirname, './migrations'), + context: { + plugin: this, + }, + }); + } async install() { const repository = this.app.db.getRepository('uiRoutes'); @@ -13,6 +22,35 @@ export class MobileClientPlugin extends Plugin { values, }); } + const uiSchemas = this.db.getRepository('uiSchemas'); + const systemSettings = this.db.getRepository('systemSettings'); + const schema = await uiSchemas.insert({ + type: 'void', + 'x-component': 'MContainer', + 'x-designer': 'MContainer.Designer', + 'x-component-props': {}, + properties: { + page: { + type: 'void', + 'x-component': 'MPage', + 'x-designer': 'MPage.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'MBlockInitializers', + 'x-component-props': { + showDivider: false, + }, + }, + }, + }, + }, + }); + const instance = await systemSettings.findOne(); + instance.set('options.mobileSchemaUid', schema['x-uid']); + await instance.save(); } async afterEnable() {} From c240228a698f81d238e2fb9e987ecac204d79d9a Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Wed, 21 Jun 2023 16:00:39 +0800 Subject: [PATCH 05/26] fix: unable to load data from chinaRegion during the first configuation (#2089) Close T-607 --- .../client/src/schema-component/antd/cascader/Cascader.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx index bd94c8d34a..c6ec9cf5ea 100644 --- a/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx +++ b/packages/core/client/src/schema-component/antd/cascader/Cascader.tsx @@ -2,10 +2,9 @@ import { LoadingOutlined } from '@ant-design/icons'; import { ArrayField } from '@formily/core'; import { connect, mapProps, mapReadPretty, useField } from '@formily/react'; import { toArr } from '@formily/shared'; -import { action } from '@formily/reactive'; import { Cascader as AntdCascader, Space } from 'antd'; import { isBoolean, omit } from 'lodash'; -import React, { useState } from 'react'; +import React from 'react'; import { useRequest } from '../../../api-client'; import { defaultFieldNames } from './defaultFieldNames'; import { ReadPretty } from './ReadPretty'; @@ -64,7 +63,7 @@ export const Cascader = connect( ); }; const handelDropDownVisible = (value) => { - if (value && !field.dataSource) { + if (value && !field.dataSource.length) { run(); } }; From 6a589543f9df318caf21905da5dddc3b378eb8ac Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 21 Jun 2023 15:37:06 +0700 Subject: [PATCH 06/26] refactor(db): add batch logic to update for better performance (#2070) * refactor(db): add batch logic to update for better performance * test(plugin-workflow): fix test cases * fix(db): treat belongsTo field in update values as foreignKey * fix(db): also handle object with id for belongsTo field * fix(db): avoid 0 as falsy * fix(db): fix test case --- .../database/src/__tests__/repository.test.ts | 123 +++++++++++++- .../src/__tests__/update-guard.test.ts | 2 + packages/core/database/src/repository.ts | 39 +++++ packages/core/database/src/update-guard.ts | 14 +- .../client/components/CollectionFieldset.tsx | 27 ++-- .../workflow/src/client/locale/zh-CN.ts | 7 + .../workflow/src/client/nodes/update.tsx | 65 +++++++- .../plugins/workflow/src/server/Plugin.ts | 5 +- .../__tests__/instructions/update.test.ts | 153 ++++++++++++++---- .../src/server/instructions/update.ts | 4 +- 10 files changed, 383 insertions(+), 56 deletions(-) diff --git a/packages/core/database/src/__tests__/repository.test.ts b/packages/core/database/src/__tests__/repository.test.ts index b0ffc6c904..8940b13da5 100644 --- a/packages/core/database/src/__tests__/repository.test.ts +++ b/packages/core/database/src/__tests__/repository.test.ts @@ -398,6 +398,7 @@ describe('repository.update', () => { fields: [ { type: 'string', name: 'name' }, { type: 'hasMany', name: 'comments' }, + { type: 'belongsTo', name: 'user' }, ], }); Comment = db.collection({ @@ -411,7 +412,7 @@ describe('repository.update', () => { await db.close(); }); - it('update1', async () => { + it('update with filterByTk and with associations', async () => { const user = await User.model.create({ name: 'user1', }); @@ -454,7 +455,7 @@ describe('repository.update', () => { expect(updated2.posts.length).toBe(2); }); - it('update2', async () => { + it('update with filterByTk', async () => { const user = await User.model.create({ name: 'user1', }); @@ -463,6 +464,9 @@ describe('repository.update', () => { name: 'user2', }); + const hook = jest.fn(); + db.on('users.afterUpdate', hook); + await User.repository.update({ filterByTk: user.id, values: { @@ -470,6 +474,8 @@ describe('repository.update', () => { }, }); + expect(hook).toBeCalledTimes(1); + const updated = await User.model.findByPk(user.id); expect(updated.get('name')).toEqual('user11'); @@ -477,6 +483,119 @@ describe('repository.update', () => { const u2 = await User.model.findByPk(user2.id); expect(u2.get('name')).toEqual('user2'); }); + + it('update with filter one by one when individualHooks is not set', async () => { + const u1 = await User.repository.create({ values: { name: 'u1' } }); + + const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } }); + const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } }); + const p3 = await Post.repository.create({ values: { name: 'p3' } }); + + const hook = jest.fn(); + db.on('posts.afterUpdate', hook); + + await Post.repository.update({ + filter: { + userId: u1.id, + }, + values: { + name: 'pp', + }, + }); + + const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] }); + expect(postsAfterUpdated[0].name).toBe('pp'); + expect(postsAfterUpdated[1].name).toBe('pp'); + expect(postsAfterUpdated[2].name).toBe('p3'); + + expect(hook).toBeCalledTimes(2); + }); + + it('update in batch when individualHooks is false', async () => { + const u1 = await User.repository.create({ values: { name: 'u1' } }); + + const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } }); + const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } }); + const p3 = await Post.repository.create({ values: { name: 'p3' } }); + + const hook = jest.fn(); + db.on('posts.afterUpdate', hook); + + await Post.repository.update({ + filter: { + userId: u1.id, + }, + values: { + name: 'pp', + }, + individualHooks: false, + }); + + const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] }); + expect(postsAfterUpdated[0].name).toBe('pp'); + expect(postsAfterUpdated[1].name).toBe('pp'); + expect(postsAfterUpdated[2].name).toBe('p3'); + + expect(hook).toBeCalledTimes(0); + }); + + it('update in batch with belongsTo field as foreignKey', async () => { + const u1 = await User.repository.create({ values: { name: 'u1' } }); + const u2 = await User.repository.create({ values: { name: 'u2' } }); + const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } }); + const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } }); + + const r1 = await Post.repository.update({ + filter: { + name: p1.name, + }, + values: { + user: u2.id, + }, + individualHooks: false, + }); + + expect(r1).toEqual(1); + + const p1Updated = await Post.repository.findOne({ + filterByTk: p1.id, + }); + expect(p1Updated.userId).toBe(u2.id); + + const r2 = await Post.repository.update({ + filter: { + id: p2.id, + }, + values: { + user: null, + }, + individualHooks: false, + }); + + expect(r2).toEqual(1); + + const p2Updated = await Post.repository.findOne({ + filterByTk: p2.id, + }); + expect(p2Updated.userId).toBe(null); + + const r3 = await Post.repository.update({ + filter: { + id: p1.id, + }, + values: { + user: { id: u1.id }, + }, + individualHooks: false, + }); + + expect(r3).toEqual(1); + + const p1Updated2 = await Post.repository.findOne({ + filterByTk: p1.id, + }); + expect(p1Updated2.userId).toBe(u1.id); + }); }); describe('repository.destroy', () => { diff --git a/packages/core/database/src/__tests__/update-guard.test.ts b/packages/core/database/src/__tests__/update-guard.test.ts index acc15a1f62..09f5da29e5 100644 --- a/packages/core/database/src/__tests__/update-guard.test.ts +++ b/packages/core/database/src/__tests__/update-guard.test.ts @@ -370,6 +370,7 @@ describe('One2One Association', () => { uid: 1, name: '123', }, + userId: 1, }; const guard = new UpdateGuard(); @@ -381,6 +382,7 @@ describe('One2One Association', () => { user: { uid: 1, }, + userId: 1, }); guard.setAssociationKeysToBeUpdate(['user']); diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index 606194a1e9..57e1f5890c 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -609,6 +609,45 @@ export class Repository { + return ( + Object.keys(include.where || {}).length > 0 || + JSON.stringify(queryOptions?.filter)?.includes(include.association) + ); + }), + transaction, + }); + const [result] = await Model.update(values, { + where: { + [primaryKeyField]: rows.map((row) => row.get(primaryKeyField)), + }, + fields: options.fields, + hooks: options.hooks, + validate: options.validate, + sideEffects: options.sideEffects, + limit: options.limit, + silent: options.silent, + transaction, + }); + // TODO: not support association fields except belongsTo + return result; + } + const instances = await this.find({ ...queryOptions, transaction, diff --git a/packages/core/database/src/update-guard.ts b/packages/core/database/src/update-guard.ts index de059b7672..a46f49c425 100644 --- a/packages/core/database/src/update-guard.ts +++ b/packages/core/database/src/update-guard.ts @@ -93,6 +93,8 @@ export class UpdateGuard { Object.keys(associationsValues).forEach((association) => { let associationValues = associationsValues[association]; + const associationObj = associations[association]; + const filterAssociationToBeUpdate = (value) => { if (value === null) { return value; @@ -104,8 +106,6 @@ export class UpdateGuard { return value; } - const associationObj = associations[association]; - const associationKeyName = associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne' ? (associationObj).targetKey @@ -143,6 +143,16 @@ export class UpdateGuard { // set association values to sanitized value values[association] = associationValues; + + if (associationObj.associationType === 'BelongsTo') { + if (typeof associationValues === 'object' && associationValues !== null) { + if (associationValues[(associationObj as any).targetKey] != null) { + values[(associationObj as any).foreignKey] = associationValues[(associationObj as any).targetKey]; + } + } else { + values[(associationObj as any).foreignKey] = associationValues; + } + } }); if (values instanceof Model) { diff --git a/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx b/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx index 65319bed04..3e10de00a1 100644 --- a/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx +++ b/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { observer, useForm, useField } from '@formily/react'; import { Input, Button, Dropdown, Menu, Form } from 'antd'; import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons'; @@ -32,25 +32,28 @@ function AssociationInput(props) { return ; } +export function useCollectionUIFields(collection) { + const { getCollectionFields } = useCollectionManager(); + + return getCollectionFields(collection).filter( + (field) => !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false), + ); +} + // NOTE: observer for watching useProps const CollectionFieldSet = observer( - ({ value, disabled, onChange }: any) => { + ({ value, disabled, onChange, filter }: any) => { const { t } = useTranslation(); const compile = useCompile(); const form = useForm(); - const { getCollection, getCollectionFields } = useCollectionManager(); - const { values: config } = useForm(); + const { getCollection } = useCollectionManager(); + const scope = useWorkflowVariableOptions(); + const { values: config } = form; const collectionName = config?.collection; - const fields = getCollectionFields(collectionName).filter( - (field) => - !field.hidden && - (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false) && - // TODO: should use some field option but not type to control this - !['formula'].includes(field.type), - ); + const collectionFields = useCollectionUIFields(collectionName); + const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields; const unassignedFields = fields.filter((field) => !value || !(field.name in value)); - const scope = useWorkflowVariableOptions(); const mergedDisabled = disabled || form.disabled; return ( diff --git a/packages/plugins/workflow/src/client/locale/zh-CN.ts b/packages/plugins/workflow/src/client/locale/zh-CN.ts index 7fdee99685..37f4c443f2 100644 --- a/packages/plugins/workflow/src/client/locale/zh-CN.ts +++ b/packages/plugins/workflow/src/client/locale/zh-CN.ts @@ -171,6 +171,13 @@ export default { 'Update record': '更新数据', 'Update records of a collection. You can use variables from upstream nodes as query conditions and field values.': '更新一个数据表中的数据。可以使用上游节点里的变量作为查询条件和数据值。', + 'Update mode': '更新模式', + 'Update in a batch': '批量更新', + 'Update one by one': '逐条更新', + 'Update all eligible data at one time, which has better performance when the amount of data is large. But the updated data will not trigger other workflows, and will not record audit logs.': + '一次性更新所有符合条件的数据,在数据量较大时有比较好的性能;但被更新的数据不会触发其他工作流,也不会记录更新日志。', + 'The updated data can trigger other workflows, and the audit log will also be recorded. But it is usually only applicable to several or dozens of pieces of data, otherwise there will be performance problems.': + '被更新的数据可以再次触发其他工作流,也会记录更新日志;但通常只适用于数条或数十条数据,否则会有性能问题。', 'Query record': '查询数据', 'Query records from a collection. You can use variables from upstream nodes as query conditions.': '查询一个数据表中的数据。可以使用上游节点里的变量作为查询条件。', diff --git a/packages/plugins/workflow/src/client/nodes/update.tsx b/packages/plugins/workflow/src/client/nodes/update.tsx index 760f19323f..06588c6afa 100644 --- a/packages/plugins/workflow/src/client/nodes/update.tsx +++ b/packages/plugins/workflow/src/client/nodes/update.tsx @@ -1,11 +1,43 @@ +import React from 'react'; +import { onFieldInputValueChange } from '@formily/core'; +import { useForm, useField } from '@formily/react'; + import { useCollectionDataSource } from '@nocobase/client'; import { FilterDynamicComponent } from '../components/FilterDynamicComponent'; -import CollectionFieldset from '../components/CollectionFieldset'; +import CollectionFieldset, { useCollectionUIFields } from '../components/CollectionFieldset'; import { isValidFilter } from '../utils'; import { NAMESPACE } from '../locale'; import { collection, filter, values } from '../schemas/collection'; +import { RadioWithTooltip } from '../components/RadioWithTooltip'; + +function IndividualHooksRadioWithTooltip({ onChange, ...props }) { + const form = useForm(); + const { collection } = form.values; + const fields = useCollectionUIFields(collection); + const field = useField(); + + function onValueChange({ target }) { + const valuesField = field.query('.values').take(); + if (!valuesField) { + return; + } + const filteredValues = fields.reduce((result, item) => { + if ( + item.name in valuesField.value && + (target.value || !['hasOne', 'hasMany', 'belongsToMany'].includes(item.type)) + ) { + result[item.name] = valuesField.value[item.name]; + } + return result; + }, {}); + form.setValuesIn('params.values', filteredValues); + + onChange(target.value); + } + return ; +} export default { title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`, @@ -17,6 +49,27 @@ export default { params: { type: 'object', properties: { + individualHooks: { + type: 'boolean', + title: `{{t("Update mode", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'IndividualHooksRadioWithTooltip', + 'x-component-props': { + options: [ + { + label: `{{t("Update in a batch", { ns: "${NAMESPACE}" })}}`, + value: false, + tooltip: `{{t("Update all eligible data at one time, which has better performance when the amount of data is large. But the updated data will not trigger other workflows, and will not record audit logs.", { ns: "${NAMESPACE}" })}}`, + }, + { + label: `{{t("Update one by one", { ns: "${NAMESPACE}" })}}`, + value: true, + tooltip: `{{t("The updated data can trigger other workflows, and the audit log will also be recorded. But it is usually only applicable to several or dozens of pieces of data, otherwise there will be performance problems.", { ns: "${NAMESPACE}" })}}`, + }, + ], + }, + default: false, + }, filter: { ...filter, title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`, @@ -24,7 +77,14 @@ export default { return isValidFilter(value) ? '' : `{{t("Please add at least one condition", { ns: "${NAMESPACE}" })}}`; }, }, - values, + values: { + ...values, + 'x-component-props': { + filter(this, field) { + return this.params?.individualHooks || !['hasOne', 'hasMany', 'belongsToMany'].includes(field.type); + }, + }, + }, }, }, }, @@ -35,5 +95,6 @@ export default { components: { FilterDynamicComponent, CollectionFieldset, + IndividualHooksRadioWithTooltip, }, }; diff --git a/packages/plugins/workflow/src/server/Plugin.ts b/packages/plugins/workflow/src/server/Plugin.ts index fb1104030f..e426025a0b 100644 --- a/packages/plugins/workflow/src/server/Plugin.ts +++ b/packages/plugins/workflow/src/server/Plugin.ts @@ -201,8 +201,9 @@ export default class WorkflowPlugin extends Plugin { this.events.push([workflow, context, options]); - this.getLogger(workflow.id).debug(`new event triggered, now events: ${this.events.length}`, { - data: workflow.config, + this.getLogger(workflow.id).info(`new event triggered, now events: ${this.events.length}`); + this.getLogger(workflow.id).debug(`event data:`, { + data: context, }); if (this.events.length > 1) { diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/update.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/update.test.ts index 2a80d62123..69a104fedf 100644 --- a/packages/plugins/workflow/src/server/__tests__/instructions/update.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/instructions/update.test.ts @@ -18,7 +18,6 @@ describe('workflow > instructions > update', () => { PostRepo = db.getCollection('posts').repository; workflow = await WorkflowModel.create({ - title: 'test workflow', enabled: true, type: 'collection', config: { @@ -54,51 +53,137 @@ describe('workflow > instructions > update', () => { const [execution] = await workflow.getExecutions(); const [job] = await execution.getJobs(); - expect(job.result.published).toBe(true); + expect(job.result).toBe(1); const updatedPost = await PostRepo.findById(post.id); expect(updatedPost.published).toBe(true); }); + + it('params: from job of node', async () => { + const n1 = await workflow.createNode({ + type: 'query', + config: { + collection: 'posts', + params: { + filter: { + title: 'test', + }, + }, + }, + }); + + const n2 = await workflow.createNode({ + type: 'update', + config: { + collection: 'posts', + params: { + filter: { + id: `{{$jobsMapByNodeId.${n1.id}.id}}`, + }, + values: { + title: 'changed', + }, + }, + }, + upstreamId: n1.id, + }); + + await n1.setDownstream(n2); + + // NOTE: the result of post immediately created will not be changed by workflow + const { id } = await PostRepo.create({ values: { title: 'test' } }); + + await sleep(500); + + // should get from db + const post = await PostRepo.findById(id); + expect(post.title).toBe('changed'); + }); }); - it('params: from job of node', async () => { - const n1 = await workflow.createNode({ - type: 'query', - config: { - collection: 'posts', - params: { - filter: { - title: 'test', + describe('update batch', () => { + it('individualHooks off should not trigger other workflow', async () => { + const w2 = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 2, + collection: 'posts', + }, + }); + + const n1 = await workflow.createNode({ + type: 'update', + config: { + collection: 'posts', + params: { + filter: { + id: '{{$context.data.id}}', + }, + values: { + published: true, + }, + individualHooks: false, }, }, - }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + expect(post.published).toBe(false); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + const [job] = await execution.getJobs(); + expect(job.result).toBe(1); + + const updatedPost = await PostRepo.findById(post.id); + expect(updatedPost.published).toBe(true); + + const w2Exes = await w2.getExecutions(); + expect(w2Exes.length).toBe(0); }); - const n2 = await workflow.createNode({ - type: 'update', - config: { - collection: 'posts', - params: { - filter: { - id: `{{$jobsMapByNodeId.${n1.id}.id}}`, - }, - values: { - title: 'changed', + it('individualHooks on should trigger other workflow', async () => { + const w2 = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 2, + collection: 'posts', + }, + }); + + const n1 = await workflow.createNode({ + type: 'update', + config: { + collection: 'posts', + params: { + filter: { + id: '{{$context.data.id}}', + }, + values: { + published: true, + }, + individualHooks: true, }, }, - }, - upstreamId: n1.id, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + expect(post.published).toBe(false); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + const [job] = await execution.getJobs(); + expect(job.result).toBe(1); + + const updatedPost = await PostRepo.findById(post.id); + expect(updatedPost.published).toBe(true); + + const w2Exes = await w2.getExecutions(); + expect(w2Exes.length).toBe(1); }); - - await n1.setDownstream(n2); - - // NOTE: the result of post immediately created will not be changed by workflow - const { id } = await PostRepo.create({ values: { title: 'test' } }); - - await sleep(500); - - // should get from db - const post = await PostRepo.findById(id); - expect(post.title).toBe('changed'); }); }); diff --git a/packages/plugins/workflow/src/server/instructions/update.ts b/packages/plugins/workflow/src/server/instructions/update.ts index 6ac3ee371c..94848172e0 100644 --- a/packages/plugins/workflow/src/server/instructions/update.ts +++ b/packages/plugins/workflow/src/server/instructions/update.ts @@ -4,7 +4,7 @@ import { JOB_STATUS } from '../constants'; export default { async run(node: FlowNodeModel, input, processor: Processor) { - const { collection, multiple = false, params = {} } = node.config; + const { collection, params = {} } = node.config; const repo = (node.constructor).database.getRepository(collection); const options = processor.getParsedValue(params, node); @@ -17,7 +17,7 @@ export default { }); return { - result: multiple ? result : result[0] || null, + result: result.length ?? result, status: JOB_STATUS.RESOLVED, }; }, From daa48302df457249156090e15f923945b2d7a72a Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 21 Jun 2023 16:37:56 +0800 Subject: [PATCH 07/26] fix(acl): change route.uiSchemaUid to useAdminSchemaUid --- .../src/acl/Configuration/MenuItemsProvider.tsx | 13 +++++++++---- .../src/route-switch/antd/admin-layout/index.tsx | 2 -- .../mobile-client/src/client/router/Application.tsx | 2 -- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx index 7e79aed15d..d3cf7e2918 100644 --- a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx +++ b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx @@ -1,7 +1,7 @@ import { Spin } from 'antd'; import React, { createContext, useContext } from 'react'; -import { useRequest, useAPIClient } from '../../api-client'; -import { useRoute } from '../../route-switch'; +import { useRequest } from '../../api-client'; +import { useSystemSettings } from '../../system-settings'; const MenuItemsContext = createContext(null); @@ -27,10 +27,15 @@ export const useMenuItems = () => { return useContext(MenuItemsContext); }; +const useAdminSchemaUid = () => { + const ctx = useSystemSettings(); + return ctx?.data?.data?.options?.adminSchemaUid; +}; + export const MenuItemsProvider = (props) => { - const route = useRoute(); + const adminSchemaUid = useAdminSchemaUid(); const options = { - url: `uiSchemas:getProperties/${route.uiSchemaUid}`, + url: `uiSchemas:getProperties/${adminSchemaUid}`, }; const service = useRequest(options); if (service.loading) { diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 8dee2a0563..ab0f3a9c16 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -16,7 +16,6 @@ import { useACLRoleContext, useDocumentTitle, useRequest, - useRoute, useSystemSettings, } from '../../../'; import { useCollectionManager } from '../../../collection-manager'; @@ -67,7 +66,6 @@ const MenuEditor = (props) => { const defaultSelectedUid = params.name; const { sideMenuRef } = props; const ctx = useACLRoleContext(); - const route = useRoute(); const [current, setCurrent] = useState(null); const onSelect = ({ item }) => { const schema = item.props.schema; diff --git a/packages/plugins/mobile-client/src/client/router/Application.tsx b/packages/plugins/mobile-client/src/client/router/Application.tsx index cb622fcc57..8af646b32d 100644 --- a/packages/plugins/mobile-client/src/client/router/Application.tsx +++ b/packages/plugins/mobile-client/src/client/router/Application.tsx @@ -3,7 +3,6 @@ import { ActionContextProvider, AdminProvider, RemoteSchemaComponent, - useRoute, useSystemSettings, useViewport, } from '@nocobase/client'; @@ -79,7 +78,6 @@ const useMobileSchemaUid = () => { }; const MApplication: React.FC = (props) => { - const route = useRoute(); const mobileSchemaUid = useMobileSchemaUid(); console.log('mobileSchemaUid', mobileSchemaUid); const params = useParams<{ name: string }>(); From d7ed43b86d7068e6359e164ed5dfb58267356bc3 Mon Sep 17 00:00:00 2001 From: jack zhang <1098626505@qq.com> Date: Wed, 21 Jun 2023 16:40:21 +0800 Subject: [PATCH 08/26] fix: add useAdminSchemaUid (#2092) Co-authored-by: chenos --- .../core/client/src/acl/Configuration/MenuItemsProvider.tsx | 2 +- packages/core/client/src/hooks/index.ts | 1 + packages/core/client/src/hooks/useAdminSchemaUid.ts | 6 ++++++ .../client/src/route-switch/antd/admin-layout/index.tsx | 6 +----- 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 packages/core/client/src/hooks/useAdminSchemaUid.ts diff --git a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx index d3cf7e2918..1c533215a8 100644 --- a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx +++ b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx @@ -1,7 +1,7 @@ import { Spin } from 'antd'; import React, { createContext, useContext } from 'react'; import { useRequest } from '../../api-client'; -import { useSystemSettings } from '../../system-settings'; +import { useAdminSchemaUid } from '../../hooks'; const MenuItemsContext = createContext(null); diff --git a/packages/core/client/src/hooks/index.ts b/packages/core/client/src/hooks/index.ts index 9319c84538..5f1aed6833 100644 --- a/packages/core/client/src/hooks/index.ts +++ b/packages/core/client/src/hooks/index.ts @@ -1 +1,2 @@ +export * from './useAdminSchemaUid'; export * from './useViewport'; diff --git a/packages/core/client/src/hooks/useAdminSchemaUid.ts b/packages/core/client/src/hooks/useAdminSchemaUid.ts new file mode 100644 index 0000000000..c5904b60e9 --- /dev/null +++ b/packages/core/client/src/hooks/useAdminSchemaUid.ts @@ -0,0 +1,6 @@ +import { useSystemSettings } from '../system-settings'; + +export const useAdminSchemaUid = () => { + const ctx = useSystemSettings(); + return ctx?.data?.data?.options?.adminSchemaUid; +}; diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index ab0f3a9c16..9d37094ac7 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -14,6 +14,7 @@ import { findByUid, findMenuItem, useACLRoleContext, + useAdminSchemaUid, useDocumentTitle, useRequest, useSystemSettings, @@ -54,11 +55,6 @@ const useMenuProps = () => { }; }; -const useAdminSchemaUid = () => { - const ctx = useSystemSettings(); - return ctx?.data?.data?.options?.adminSchemaUid; -}; - const MenuEditor = (props) => { const { setTitle } = useDocumentTitle(); const navigate = useNavigate(); From dae191691c0259d1742eccfa541bbb2d9799a7eb Mon Sep 17 00:00:00 2001 From: dream2023 <1098626505@qq.com> Date: Wed, 21 Jun 2023 17:49:45 +0800 Subject: [PATCH 09/26] fix: useAdminSchemaUid redeclaration --- .../core/client/src/acl/Configuration/MenuItemsProvider.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx index 1c533215a8..fedadb2a1c 100644 --- a/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx +++ b/packages/core/client/src/acl/Configuration/MenuItemsProvider.tsx @@ -27,11 +27,6 @@ export const useMenuItems = () => { return useContext(MenuItemsContext); }; -const useAdminSchemaUid = () => { - const ctx = useSystemSettings(); - return ctx?.data?.data?.options?.adminSchemaUid; -}; - export const MenuItemsProvider = (props) => { const adminSchemaUid = useAdminSchemaUid(); const options = { From 1006a66a6f3908d7d5d8c585bb2c4b7a1dca4bee Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Wed, 21 Jun 2023 18:29:14 +0800 Subject: [PATCH 10/26] fix: incomplete field list for assigned fields (#2093) --- .../components/assigned-field/AssignedField.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx index ba9e061361..c3665f22d8 100644 --- a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx +++ b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx @@ -89,6 +89,7 @@ export enum AssignedFieldValueType { export const AssignedField = (props: any) => { const { t } = useTranslation(); const compile = useCompile(); + const collection = useCollection(); const field = useField(); const fieldSchema = useFieldSchema(); const isDynamicValue = @@ -110,7 +111,7 @@ export const AssignedField = (props: any) => { const [options, setOptions] = useState([]); const { getField } = useCollection(); const collectionField = getField(fieldSchema.name); - const fields = useCollectionFilterOptions(collectionField?.collectionName); + const fields = useCollectionFilterOptions(collection?.name); const userFields = useCollectionFilterOptions('users'); const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt']; useEffect(() => { From 6254fceb049b89f08bb8af4771f907072a87037d Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 21 Jun 2023 20:59:19 +0700 Subject: [PATCH 11/26] fix(plugin-verification): fix duplication of installation (#2097) --- packages/plugins/verification/src/server/Plugin.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/plugins/verification/src/server/Plugin.ts b/packages/plugins/verification/src/server/Plugin.ts index 503a53754f..512d456b88 100644 --- a/packages/plugins/verification/src/server/Plugin.ts +++ b/packages/plugins/verification/src/server/Plugin.ts @@ -98,6 +98,12 @@ export default class VerificationPlugin extends Plugin { INIT_ALI_SMS_VERIFY_CODE_SIGN ) { const ProviderRepo = this.db.getRepository('verifications_providers'); + const existed = await ProviderRepo.count({ + filterByTk: DEFAULT_SMS_VERIFY_CODE_PROVIDER, + }); + if (existed) { + return; + } await ProviderRepo.create({ values: { id: DEFAULT_SMS_VERIFY_CODE_PROVIDER, From b80aaacb38a245206e3ef27da7eb44ce7341867a Mon Sep 17 00:00:00 2001 From: Dunqing Date: Wed, 21 Jun 2023 22:26:15 +0800 Subject: [PATCH 12/26] fix(mobile-client): fix multiple bugs and do some improvement (#2072) --- .../src/schema-component/antd/index.less | 4 +- .../src/schema-component/antd/tabs/Tabs.tsx | 27 ++-- .../schema-component/antd/tabs/context.tsx | 2 +- .../src/client/core/bridge/injects.ts | 11 +- .../core/schema/components/menu/Menu.Item.tsx | 20 +-- .../core/schema/components/menu/schema.ts | 6 +- .../core/schema/components/page/Page.tsx | 133 ++++++++++-------- .../src/client/router/Application.tsx | 1 + .../src/client/router/RouteSwitchProvider.tsx | 2 +- 9 files changed, 103 insertions(+), 103 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/index.less b/packages/core/client/src/schema-component/antd/index.less index 33c59fee38..4e13ff4b97 100644 --- a/packages/core/client/src/schema-component/antd/index.less +++ b/packages/core/client/src/schema-component/antd/index.less @@ -42,7 +42,5 @@ } html body { - --adm-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Hiragino Sans GB, - Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, - Segoe UI Symbol; + --adm-font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; } diff --git a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx index 7d08eabe97..64e0e7aec2 100644 --- a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx +++ b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx @@ -2,45 +2,36 @@ import { css } from '@emotion/css'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd'; import classNames from 'classnames'; -import React, { useMemo } from 'react'; +import React from 'react'; import { Icon } from '../../../icon'; import { useSchemaInitializer } from '../../../schema-initializer'; import { DndContext, SortableItem } from '../../common'; import { useDesigner } from '../../hooks/useDesigner'; -import { TabsContextProvider, useTabsContext } from './context'; +import { useTabsContext } from './context'; import { TabsDesigner } from './Tabs.Designer'; +import { useDesignable } from '../../hooks'; export const Tabs: any = observer( (props: TabsProps) => { const fieldSchema = useFieldSchema(); const { render } = useSchemaInitializer(fieldSchema['x-initializer']); + const { designable } = useDesignable(); const contextProps = useTabsContext(); - - const PaneProvider = useMemo(() => { - if (contextProps.deep === false) { - return TabsContextProvider; - } - return React.Fragment; - }, [contextProps.deep]); + const { PaneRoot = React.Fragment as React.FC } = contextProps; return ( - + {fieldSchema.mapProperties((schema, key) => { return ( } key={key}> - + - + ); })} + {designable && } ); diff --git a/packages/core/client/src/schema-component/antd/tabs/context.tsx b/packages/core/client/src/schema-component/antd/tabs/context.tsx index 24d05eddc8..e11dddba76 100644 --- a/packages/core/client/src/schema-component/antd/tabs/context.tsx +++ b/packages/core/client/src/schema-component/antd/tabs/context.tsx @@ -2,7 +2,7 @@ import { TabsProps } from 'antd'; import React from 'react'; interface TabsContextProps extends TabsProps { - deep?: boolean; + PaneRoot?: React.FC; } const TabsContext = React.createContext({}); diff --git a/packages/plugins/mobile-client/src/client/core/bridge/injects.ts b/packages/plugins/mobile-client/src/client/core/bridge/injects.ts index e4024da6ee..6b7a7d93ce 100644 --- a/packages/plugins/mobile-client/src/client/core/bridge/injects.ts +++ b/packages/plugins/mobile-client/src/client/core/bridge/injects.ts @@ -3,12 +3,13 @@ interface InvokeFunction { (params: { action: 'moveTaskToBack' }, cb?: () => void): void; } -const JsBridge = (window as any).JsBridge as { - invoke: InvokeFunction; -}; +const getJsBridge = () => + (window as any).JsBridge as { + invoke: InvokeFunction; + }; export const invoke: InvokeFunction = (params, cb) => { - JsBridge.invoke(params, cb); + return getJsBridge().invoke(params, cb); }; -export const isJSBridge = !!JsBridge; +export const isJSBridge = () => !!getJsBridge(); diff --git a/packages/plugins/mobile-client/src/client/core/schema/components/menu/Menu.Item.tsx b/packages/plugins/mobile-client/src/client/core/schema/components/menu/Menu.Item.tsx index 87798d0797..3b81173c46 100644 --- a/packages/plugins/mobile-client/src/client/core/schema/components/menu/Menu.Item.tsx +++ b/packages/plugins/mobile-client/src/client/core/schema/components/menu/Menu.Item.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from '../../../../locale'; import { useSchemaPatch } from '../../hooks'; +import { menuItemSchema } from './schema'; interface MMenuItemProps extends ListItemProps { name: string; @@ -56,24 +57,7 @@ const MenuItemDesigner: React.FC = () => { { // Only support globalActions in page const onlyInPage = fieldSchema.root === fieldSchema.parent; let hasGlobalActions = false; - if (!tabsSchema) { + if (!tabsSchema && fieldSchema.properties) { hasGlobalActions = countGridCol(fieldSchema.properties['grid'], 2) === 1; - } else if (searchParams.has('tab') && tabsSchema.properties?.[searchParams.get('tab')]) { + } else if (searchParams.has('tab') && tabsSchema?.properties?.[searchParams.get('tab')]) { hasGlobalActions = countGridCol(tabsSchema.properties[searchParams.get('tab')]?.properties?.['grid'], 2) === 1; - } else if (tabsSchema.properties) { + } else if (tabsSchema?.properties) { const schema = Object.values(tabsSchema.properties).sort((t1, t2) => t1['x-index'] - t2['x-index'])[0]; if (schema) { setTimeout(() => { - setSearchParams([['tab', schema.name.toString()]]); + setSearchParams([['tab', schema.name.toString()]], { + replace: true, + }); }); } } const onTabsChange = useCallback( (key) => { - setSearchParams([['tab', key]]); + setSearchParams([['tab', key]], { + replace: true, + }); }, [setSearchParams], ); - const GlobalActionProvider = useMemo(() => { - if (hasGlobalActions) { - return ActionBarProvider; - } - return (props) => <>{props.children}; - }, [hasGlobalActions]); + const GlobalActionProvider = useCallback( + (props) => { + if (hasGlobalActions) { + return ( + + + {props.children} + + + ); + } + return <>{props.children}; + }, + [hasGlobalActions, onlyInPage], + ); return ( - - +
.ant-tabs > .ant-tabs-nav { + .ant-tabs-tab { + margin: 0 !important; + padding: 0 16px !important; + } + background: #fff; + } display: flex; flex-direction: column; - width: 100%; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - padding-bottom: var(--nb-spacing); `, )} > - -
{ + return 'MHeader' === s['x-component']; }} - className={cx( - 'nb-mobile-page-header', - css` - & > .ant-tabs > .ant-tabs-nav { - background: #fff; - padding: 0 var(--nb-spacing); - } - display: flex; - flex-direction: column; - `, - )} + > + { - return 'MHeader' === s['x-component']; + return 'Tabs' === s['x-component']; }} > - - { - return 'Tabs' === s['x-component']; - }} - > - -
- + +
+ {!tabsSchema && ( { }} > )} -
-
+ + ); }; diff --git a/packages/plugins/mobile-client/src/client/router/Application.tsx b/packages/plugins/mobile-client/src/client/router/Application.tsx index 8af646b32d..6f74f7710c 100644 --- a/packages/plugins/mobile-client/src/client/router/Application.tsx +++ b/packages/plugins/mobile-client/src/client/router/Application.tsx @@ -115,6 +115,7 @@ const MApplication: React.FC = (props) => { {props.children}
)} + {/* Global action will insert here */}
diff --git a/packages/plugins/mobile-client/src/client/router/RouteSwitchProvider.tsx b/packages/plugins/mobile-client/src/client/router/RouteSwitchProvider.tsx index 19bfcc2c06..12e432dc1c 100644 --- a/packages/plugins/mobile-client/src/client/router/RouteSwitchProvider.tsx +++ b/packages/plugins/mobile-client/src/client/router/RouteSwitchProvider.tsx @@ -10,7 +10,7 @@ export const RouterSwitchProvider = (props) => { const location = useLocation(); const navigate = useNavigate(); useEffect(() => { - if (isJSBridge) { + if (isJSBridge()) { if (location.pathname.includes('/admin')) { navigate('/mobile'); } From 5fc5428d03b6076dc8e171395bae72a24e7fc42d Mon Sep 17 00:00:00 2001 From: Junyi Date: Wed, 21 Jun 2023 22:03:54 +0700 Subject: [PATCH 13/26] fix(plugin-workflow): fix job button style (#2098) --- packages/plugins/workflow/src/client/nodes/index.tsx | 4 ++-- packages/plugins/workflow/src/client/style.tsx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugins/workflow/src/client/nodes/index.tsx b/packages/plugins/workflow/src/client/nodes/index.tsx index 2e0e0b7471..b374d5cd97 100644 --- a/packages/plugins/workflow/src/client/nodes/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/index.tsx @@ -240,7 +240,7 @@ function InnerJobButton({ job, ...props }) { const { icon, color } = JobStatusOptionsMap[job.status]; return ( - ); @@ -292,7 +292,7 @@ export function JobButton() { } `} > - + {icon} diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index 22a051bc11..5e7623c762 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -255,6 +255,10 @@ export const nodeJobButtonClass = css` border: none; } + &.inner{ + position: static; + } + .ant-tag { padding: 0; width: 100%; From 6eed9ac2bb8256fcf6c0550baf3f04516bc28937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rairn?= <958414905@qq.com> Date: Thu, 22 Jun 2023 19:51:16 +0800 Subject: [PATCH 14/26] refactor: fix warning of antd 4.x (#1998) * refactor: fix warning by codemod * refactor: fix warning of Dropdown * perf: use memo * refactor: resolve SchemaInitializer * refactor: fix lint * refactor: remove SettingsForm * refactor: resolve SchemaInitializer * refactor: fix lint * refactor: move useMenuItem to root dir * chore: fix conflicts * refactor: resolve SchemaSetting * refactor: fix lint * test: fix failed * chore: upgrade Vite * fix: fix style * refactor: fix lint * refactor: extract component * refactor: resovle Menu * refactor: resolve Tabs * refactor(getPopupContainer): should return the unique div * refactor(Drawer): change style to rootStyle and className to rootClassName * chore: update yarn.lock * fix: fix T-432 * fix: fix T-338 * fix: fix T-490 * fix: collection fields * fix: fix style * fix: fix T-500 * fix: fix SettingMenu error (close T-516) * fix: fix tanslation of Map (T-506) * style: fix style (T-508) * fix: fix schemaSetting switch of mobile (T-517) * fix: fix T-518 * fix: fix T-524 * fix: fix T-507 * perf: optimize SchemaInitializer.Button * perf: optimize SchemaSettings * fix: fix serch of SchemaInitializer (T-547) * chore: change delay * fix: fix button style (T-548) * fix: fix scroll bar * fix: update yarn.lock * fix: fix build error * fix: should update sideMenu when change it * fix: fix build error * chore: mouseEnterDelay * fix: fix group menu can not selected --- package.json | 2 +- packages/core/client/package.json | 1 + .../client/src/application/Application.tsx | 8 +- packages/core/client/src/auth/SigninPage.tsx | 20 +- .../Configuration/AddCollectionAction.tsx | 62 +- .../Configuration/AddFieldAction.tsx | 97 +-- .../Configuration/AddSubFieldAction.tsx | 52 +- .../Configuration/ConfigurationTabs.tsx | 99 +-- .../src/collection-manager/demos/demo5.tsx | 2 +- .../collection-manager/hooks/useOptions.ts | 71 ++- .../core/client/src/formula/Expression.tsx | 105 ++-- .../core/client/src/hooks/useMenuItem.tsx | 102 +++ packages/core/client/src/index.ts | 6 +- packages/core/client/src/pm/Card.tsx | 16 +- .../core/client/src/pm/PluginManagerLink.tsx | 50 +- packages/core/client/src/pm/index.tsx | 47 +- .../antd/action/Action.Designer.tsx | 10 +- .../antd/action/Action.Drawer.tsx | 2 +- .../antd/action/ActionBar.tsx | 2 +- .../antd/calendar/DeleteEvent.tsx | 2 +- .../antd/grid-card/GridCard.Designer.tsx | 7 +- .../src/schema-component/antd/menu/Menu.tsx | 568 ++++++++++------- .../src/schema-component/antd/page/Page.tsx | 74 ++- .../schema-component/antd/table-v2/Table.tsx | 595 +++++++++--------- .../src/schema-component/antd/tabs/Tabs.tsx | 43 +- .../antd/upload/ReadPretty.tsx | 9 +- .../antd/variable/VariableSelect.tsx | 2 +- .../schema-initializer/SchemaInitializer.tsx | 303 +++++---- .../schema-initializer/SelectCollection.tsx | 23 +- .../buttons/TableActionColumnInitializers.tsx | 2 +- .../buttons/TableActionInitializers.tsx | 10 +- .../components/CreateRecordAction.tsx | 74 +-- .../items/CalendarBlockInitializer.tsx | 9 +- .../items/InitializerWithSwitch.tsx | 3 +- .../client/src/schema-initializer/utils.ts | 31 +- .../src/schema-settings/SchemaSettings.tsx | 161 +++-- .../schema-templates/BlockTemplateDetails.tsx | 4 +- .../schema-templates/BlockTemplatePage.tsx | 4 +- .../client/src/settings-form/SettingsForm.tsx | 278 -------- .../client/src/settings-form/demos/demo1.tsx | 136 ---- .../core/client/src/settings-form/index.md | 9 - .../core/client/src/settings-form/index.ts | 1 - .../core/client/src/user/ChangePassword.tsx | 47 +- packages/core/client/src/user/CurrentUser.tsx | 176 +++--- packages/core/client/src/user/EditProfile.tsx | 47 +- .../core/client/src/user/LanguageSettings.tsx | 107 ++-- packages/core/client/src/user/SigninPage.tsx | 43 +- packages/core/client/src/user/SwitchRole.tsx | 72 ++- .../core/client/src/user/ThemeSettings.tsx | 80 +-- .../src/client/ChartQueryBlockInitializer.tsx | 199 +++--- .../src/client/settings/AddNewQuery.tsx | 47 +- .../src/client/GraphDrawPage.tsx | 100 ++- .../client/components/AddCollectionAction.tsx | 5 +- .../src/client/components/AddFieldAction.tsx | 3 +- .../components/EditCollectionAction.tsx | 5 +- .../src/client/components/EditFieldAction.tsx | 3 +- .../src/client/components/Entity.tsx | 34 +- .../client/components/OverrideFieldAction.tsx | 3 +- .../src/client/components/ViewFieldAction.tsx | 3 +- .../src/client/utils.tsx | 12 + .../src/client/IframeBlockInitializer.tsx | 2 - .../src/client/ImportInitializerProvider.tsx | 2 +- .../src/client/components/Configuration.tsx | 19 +- .../container/Container.Designer.tsx | 13 +- .../schema/components/page/Page.Designer.tsx | 11 +- .../schema/components/settings/Settings.tsx | 6 +- .../multi-app-manager/src/client/index.tsx | 2 +- .../plugins/workflow/src/client/AddButton.tsx | 138 ++-- .../workflow/src/client/WorkflowProvider.tsx | 2 +- .../client/components/CollectionFieldset.tsx | 43 +- yarn.lock | 151 +++-- 71 files changed, 2331 insertions(+), 2146 deletions(-) create mode 100644 packages/core/client/src/hooks/useMenuItem.tsx delete mode 100644 packages/core/client/src/settings-form/SettingsForm.tsx delete mode 100644 packages/core/client/src/settings-form/demos/demo1.tsx delete mode 100644 packages/core/client/src/settings-form/index.md delete mode 100644 packages/core/client/src/settings-form/index.ts diff --git a/package.json b/package.json index c466d1af17..0e405358ce 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "prettier": "^2.2.1", "pretty-format": "^24.0.0", "pretty-quick": "^3.1.0", - "vite": "^4.3.8", + "vite": "^4.3.9", "vitest": "^0.32.0" }, "volta": { diff --git a/packages/core/client/package.json b/packages/core/client/package.json index b5e529091e..7bdfe51528 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -7,6 +7,7 @@ "typings": "es/index.d.ts", "dependencies": { "@antv/g2plot": "^2.4.18", + "@ant-design/pro-layout": "^7.14.3", "@dnd-kit/core": "^5.0.1", "@dnd-kit/sortable": "^6.0.0", "@emotion/css": "^11.7.1", diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index d2b2c606e5..948fde00c8 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -7,6 +7,8 @@ import { Link, NavLink } from 'react-router-dom'; import { ACLProvider } from '../acl'; import { AntdConfigProvider } from '../antd-config-provider'; import { APIClient, APIClientProvider } from '../api-client'; +import { SigninPage, SignupPage } from '../auth'; +import { SigninPageExtensionProvider } from '../auth/SigninPageExtension'; import { BlockSchemaComponentProvider } from '../block-provider'; import { RemoteDocumentTitleProvider } from '../document-title'; import { i18n } from '../i18n'; @@ -30,8 +32,6 @@ import { ErrorFallback } from '../schema-component/antd/error-fallback'; import { SchemaInitializerProvider } from '../schema-initializer'; import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates'; import { SystemSettingsProvider } from '../system-settings'; -import { SigninPage, SignupPage } from '../auth'; -import { SigninPageExtensionProvider } from '../auth/SigninPageExtension'; import { compose } from './compose'; export interface ApplicationOptions { @@ -52,9 +52,7 @@ export type PluginCallback = () => Promise; const App = React.memo((props: any) => { const C = compose(...props.providers)(() => { const routes = useRoutes(); - return ( - - ); + return ; }); return ; }); diff --git a/packages/core/client/src/auth/SigninPage.tsx b/packages/core/client/src/auth/SigninPage.tsx index 28c7f5cbeb..5ba7bd39a1 100644 --- a/packages/core/client/src/auth/SigninPage.tsx +++ b/packages/core/client/src/auth/SigninPage.tsx @@ -1,19 +1,19 @@ +import { css } from '@emotion/css'; +import { useForm } from '@formily/react'; import { Space, Tabs } from 'antd'; import React, { + FunctionComponent, + FunctionComponentElement, + createContext, + createElement, useCallback, useContext, - createContext, - FunctionComponent, - createElement, useState, - FunctionComponentElement, } from 'react'; -import { css } from '@emotion/css'; import { useTranslation } from 'react-i18next'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useAPIClient, useCurrentDocumentTitle, useRequest, useViewport } from '..'; import { useSigninPageExtension } from './SigninPageExtension'; -import { useForm } from '@formily/react'; const SigninPageContext = createContext<{ [authType: string]: { @@ -132,13 +132,7 @@ export const SigninPage = () => { `} > {tabs.length > 1 ? ( - - {tabs.map((tab) => ( - - {tab.component} - - ))} - + ({ label: tab.tabTitle, key: tab.name, children: tab.component }))} /> ) : tabs.length ? (
{tabs[0].component}
) : ( diff --git a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx index 00924a411f..9264ee0f3e 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddCollectionAction.tsx @@ -2,9 +2,9 @@ import { DownOutlined, PlusOutlined } from '@ant-design/icons'; import { ArrayTable } from '@formily/antd'; import { ISchema, useField, useForm } from '@formily/react'; import { uid } from '@formily/shared'; -import { Button, Dropdown, Menu } from 'antd'; +import { Button, Dropdown, MenuProps } from 'antd'; import { cloneDeep } from 'lodash'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRequest } from '../../api-client'; import { RecordProvider, useRecord } from '../../record-provider'; @@ -246,41 +246,41 @@ export const AddCollectionAction = (props) => { const [schema, setSchema] = useState({}); const compile = useCompile(); const { t } = useTranslation(); - const collectionTemplates = templateOptions(); - const items = []; - collectionTemplates.forEach((item) => { - if (item.divider) { - items.push({ - type: 'divider', - }); - } - items.push({ label: compile(item.title), key: item.name }); - }); + const collectionTemplates = useMemo(templateOptions, []); + const items = useMemo(() => { + const result = []; + collectionTemplates.forEach((item) => { + if (item.divider) { + result.push({ + type: 'divider', + }); + } + result.push({ label: compile(item.title), key: item.name }); + }); + return result; + }, [collectionTemplates]); const { state: { category }, } = useResourceActionContext(); + const menu = useMemo(() => { + return { + style: { + maxHeight: '60vh', + overflow: 'auto', + }, + onClick: (info) => { + const schema = getSchema(getTemplate(info.key), category, compile); + setSchema(schema); + setVisible(true); + }, + items, + }; + }, [category, items]); + return ( - { - const schema = getSchema(getTemplate(info.key), category, compile); - setSchema(schema); - setVisible(true); - }} - items={items} - /> - } - > + {children || ( diff --git a/packages/core/client/src/collection-manager/Configuration/ConfigurationTabs.tsx b/packages/core/client/src/collection-manager/Configuration/ConfigurationTabs.tsx index 8d8324569f..39eed38685 100644 --- a/packages/core/client/src/collection-manager/Configuration/ConfigurationTabs.tsx +++ b/packages/core/client/src/collection-manager/Configuration/ConfigurationTabs.tsx @@ -11,7 +11,8 @@ import { } from '@dnd-kit/core'; import { RecursionField, observer } from '@formily/react'; import { uid } from '@formily/shared'; -import { Badge, Card, Dropdown, Menu, Modal, Tabs } from 'antd'; +import { Badge, Card, Dropdown, Modal, Tabs } from 'antd'; +import _ from 'lodash'; import React, { useContext, useState } from 'react'; import { useAPIClient } from '../../api-client'; import { SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component'; @@ -181,28 +182,37 @@ export const ConfigurationTabs = () => { value: item.id, })); }; - const menu = (item) => ( - - - { + return { + items: [ + { + key: 'edit', + label: ( + - - remove(item.id)}> - {compile("{{t('Delete category')}}")} - - - ); + }} + /> + ), + }, + { + key: 'delete', + label: compile("{{t('Delete category')}}"), + onClick: () => remove(item.id), + }, + ], + }; + }); + return ( { type="editable-card" destroyInactiveTabPane={true} tabBarStyle={{ marginBottom: '0px' }} - > - {tabsItems.map((item) => { - return ( - - - - ) : ( - compile(item.name) - ) - } - key={item.id} - closable={item.closable} - closeIcon={ - - - - } - > + items={tabsItems.map((item) => { + return { + label: + item.id !== 'all' ? ( +
+ +
+ ) : ( + compile(item.name) + ), + key: item.id, + closable: item.closable, + closeIcon: ( + + + + ), + children: ( { -
- ); + ), + }; })} -
+ />
); }; diff --git a/packages/core/client/src/collection-manager/demos/demo5.tsx b/packages/core/client/src/collection-manager/demos/demo5.tsx index c9ef536339..5eb8d8df92 100644 --- a/packages/core/client/src/collection-manager/demos/demo5.tsx +++ b/packages/core/client/src/collection-manager/demos/demo5.tsx @@ -82,7 +82,7 @@ const FormItemInitializer = (props) => { collection.fields.push(options); form.setValuesIn(name, uid()); - const { values } = await FormDrawer('Add field', () => { + await FormDrawer('Add field', () => { return ( diff --git a/packages/core/client/src/collection-manager/hooks/useOptions.ts b/packages/core/client/src/collection-manager/hooks/useOptions.ts index d737a96021..eedaad0baf 100644 --- a/packages/core/client/src/collection-manager/hooks/useOptions.ts +++ b/packages/core/client/src/collection-manager/hooks/useOptions.ts @@ -1,45 +1,48 @@ import set from 'lodash/set'; +import { useMemo } from 'react'; import { useCollectionManager } from './useCollectionManager'; export const useOptions = () => { const { interfaces } = useCollectionManager(); - const fields = {}; + return useMemo(() => { + const fields = {}; - Object.keys(interfaces).forEach((type) => { - const schema = interfaces[type]; - registerField(schema.group || 'others', type, { order: 0, ...schema }); - }); + Object.keys(interfaces).forEach((type) => { + const schema = interfaces[type]; + registerField(schema.group || 'others', type, { order: 0, ...schema }); + }); - function registerField(group: string, type: string, schema) { - fields[group] = fields[group] || {}; - set(fields, [group, type], schema); - } + function registerField(group: string, type: string, schema) { + fields[group] = fields[group] || {}; + set(fields, [group, type], schema); + } - const groupLabels = { - basic: '{{t("Basic")}}', - choices: '{{t("Choices")}}', - media: '{{t("Media")}}', - datetime: '{{t("Date & Time")}}', - relation: '{{t("Relation")}}', - advanced: '{{t("Advanced type")}}', - systemInfo: '{{t("System info")}}', - others: '{{t("Others")}}', - }; + const groupLabels = { + basic: '{{t("Basic")}}', + choices: '{{t("Choices")}}', + media: '{{t("Media")}}', + datetime: '{{t("Date & Time")}}', + relation: '{{t("Relation")}}', + advanced: '{{t("Advanced type")}}', + systemInfo: '{{t("System info")}}', + others: '{{t("Others")}}', + }; - return Object.keys(groupLabels).map((groupName) => ({ - label: groupLabels[groupName], - key: groupName, - children: Object.keys(fields[groupName] || {}) - .map((type) => { - const field = fields[groupName][type]; - return { - value: type, - label: field.title, - name: type, - ...fields[groupName][type], - }; - }) - .sort((a, b) => a.order - b.order), - })); + return Object.keys(groupLabels).map((groupName) => ({ + label: groupLabels[groupName], + key: groupName, + children: Object.keys(fields[groupName] || {}) + .map((type) => { + const field = fields[groupName][type]; + return { + value: type, + label: field.title, + name: type, + ...fields[groupName][type], + }; + }) + .sort((a, b) => a.order - b.order), + })); + }, [interfaces]); }; diff --git a/packages/core/client/src/formula/Expression.tsx b/packages/core/client/src/formula/Expression.tsx index 3eb424466a..a6e3eb0e66 100644 --- a/packages/core/client/src/formula/Expression.tsx +++ b/packages/core/client/src/formula/Expression.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; import { Field, onFormSubmitValidateStart } from '@formily/core'; import { useField, useFormEffects } from '@formily/react'; -import { Dropdown, Menu } from 'antd'; -import React, { useEffect, useRef, useState } from 'react'; +import { Dropdown, MenuProps } from 'antd'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; function pasteHtml(html, selectPastedContent = false) { @@ -71,19 +71,27 @@ export const Expression = (props) => { const inputRef = useRef(); const [changed, setChanged] = useState(false); - const onChange = (value) => { - setChanged(true); - props.onChange(value); - }; + const onChange = useCallback( + (value) => { + setChanged(true); + props.onChange(value); + }, + [props.onChange], + ); + + const { numColumns, scope } = useMemo(() => { + const numColumns = new Map(); + const scope = {}; + fields + .filter((field) => supports.includes(field.interface)) + .forEach((field) => { + numColumns.set(field.name, field.uiSchema.title); + scope[field.name] = 1; + }); + + return { numColumns, scope }; + }, [fields, supports]); - const numColumns = new Map(); - const scope = {}; - fields - .filter((field) => supports.includes(field.interface)) - .forEach((field) => { - numColumns.set(field.name, field.uiSchema.title); - scope[field.name] = 1; - }); const keys = Array.from(numColumns.keys()); const [html, setHtml] = useState(() => { const scope = {}; @@ -95,6 +103,7 @@ export const Expression = (props) => { } return renderExp(value || '', scope); }); + useEffect(() => { if (changed) { return; @@ -109,34 +118,44 @@ export const Expression = (props) => { const val = renderExp(value || '', scope); setHtml(val); }, [value]); - const menu = ( - - {keys.length > 0 ? ( - keys.map((key) => ( - - - - )) - ) : ( - - {t('No available fields')} - - )} - - ); + + const menuItems = useMemo(() => { + if (keys.length > 0) { + return keys.map((key) => ({ + key, + disabled: true, + label: ( + + ), + })); + } else { + return [ + { + key: 0, + disabled: true, + label: t('No available fields'), + }, + ]; + } + }, [keys, numColumns, onChange]); + + const menu = useMemo(() => { + return { + items: menuItems, + }; + }, [menuItems]); useFormEffects(() => { onFormSubmitValidateStart(() => { @@ -157,7 +176,7 @@ export const Expression = (props) => { return ( void }>(null); +export const GetMenuItemsContext = createContext<{ pushMenuItem?(item: Item): void }>(null); + +/** + * 用于为 SchemaInitializer.Item 组件提供一些方法,比如收集菜单项数据 + * @returns + */ +export const useCollectMenuItem = () => { + return useContext(GetMenuItemContext) || {}; +}; + +export const useCollectMenuItems = () => { + return useContext(GetMenuItemsContext) || {}; +}; + +/** + * 用于在 antd 从 4.x 升级到 5.x 中,用于把 SchemaInitializer.Item 组件这种写法转换成 Menu 组件的 items 写法 + * @returns + */ +export const useMenuItem = () => { + const list = useRef([]); + const renderItems = useRef<() => JSX.Element>(null); + const shouldRerender = useRef(false); + + const Component = useCallback(() => { + if (!shouldRerender.current) { + return null; + } + shouldRerender.current = false; + + if (renderItems.current) { + return renderItems.current(); + } + + return ( + <> + {list.current.map((Com, index) => ( + + ))} + + ); + }, []); + + const getMenuItems = useCallback((Com: () => ReactNode): Item[] => { + const items: Item[] = []; + + const pushMenuItem = (item: Item) => { + items.push(item); + items.sort((a, b) => (a.order || 0) - (b.order || 0)); + }; + + shouldRerender.current = true; + renderItems.current = () => { + const notDeleteItems = items.filter((item) => item.notdelete); + items.length = 0; + items.push(...notDeleteItems); + return ( + + {Com()} + + ); + }; + + return items; + }, []); + + const getMenuItem = useCallback((Com: () => JSX.Element): Item => { + const item = {} as Item; + + const collectMenuItem = (menuItem: Item) => { + Object.assign(item, menuItem); + }; + + shouldRerender.current = true; + list.current.push(() => { + return {Com()}; + }); + + return item; + }, []); + + // 防止 list 有重复元素 + const clean = useCallback(() => { + list.current = []; + }, []); + + return { Component, getMenuItems, getMenuItem, clean }; +}; diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 0cde6613cf..3af144005a 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -14,6 +14,7 @@ export * from './collection-manager'; export * from './document-title'; export * from './filter-provider'; export * from './formula'; +export * from './hooks'; export * from './i18n'; export * from './icon'; export * from './plugin-manager'; @@ -23,10 +24,9 @@ export * from './record-provider'; export * from './route-switch'; export * from './schema-component'; export * from './schema-initializer'; +export * from './schema-items'; export * from './schema-settings'; export * from './schema-templates'; -export * from './schema-items'; -export * from './settings-form'; export * from './system-settings'; export * from './user'; -export * from './hooks'; + diff --git a/packages/core/client/src/pm/Card.tsx b/packages/core/client/src/pm/Card.tsx index a2118db01c..bba0ff3b67 100644 --- a/packages/core/client/src/pm/Card.tsx +++ b/packages/core/client/src/pm/Card.tsx @@ -1,9 +1,8 @@ -import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react'; -import { useAPIClient, useRequest } from '../api-client'; +import { DeleteOutlined, SettingOutlined } from '@ant-design/icons'; +import { css } from '@emotion/css'; import { Avatar, Card, - message, Modal, Popconfirm, Spin, @@ -13,14 +12,15 @@ import { Tag, Tooltip, Typography, + message, } from 'antd'; -import { css } from '@emotion/css'; import cls from 'classnames'; -import { useNavigate } from 'react-router-dom'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { DeleteOutlined, SettingOutlined } from '@ant-design/icons'; -import { useParseMarkdown } from '../schema-component/antd/markdown/util'; +import { useNavigate } from 'react-router-dom'; import type { IPluginData } from '.'; +import { useAPIClient, useRequest } from '../api-client'; +import { useParseMarkdown } from '../schema-component/antd/markdown/util'; interface PluginDocumentProps { path: string; @@ -163,7 +163,7 @@ function PluginDetail(props: IPluginDetail) { destroyOnClose > {plugin?.description &&
{plugin?.description}
} - + ); } diff --git a/packages/core/client/src/pm/PluginManagerLink.tsx b/packages/core/client/src/pm/PluginManagerLink.tsx index 169d8d693a..5cffc26847 100644 --- a/packages/core/client/src/pm/PluginManagerLink.tsx +++ b/packages/core/client/src/pm/PluginManagerLink.tsx @@ -1,11 +1,12 @@ import { ApiOutlined, SettingOutlined } from '@ant-design/icons'; -import { Button, Dropdown, Menu, Tooltip } from 'antd'; -import React, { useContext, useState } from 'react'; +import { Button, Dropdown, MenuProps, Tooltip } from 'antd'; +import _ from 'lodash'; +import React, { useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useACLRoleContext } from '../acl/ACLProvider'; import { ActionContextProvider, useCompile } from '../schema-component'; -import { getPluginsTabs, SettingsCenterContext } from './index'; +import { SettingsCenterContext, getPluginsTabs } from './index'; export const PluginManagerLink = () => { const { t } = useTranslation(); @@ -23,7 +24,7 @@ export const PluginManagerLink = () => { ); }; -const getBookmarkTabs = (data) => { +const getBookmarkTabs = _.memoize((data) => { const bookmarkTabs = []; data.forEach((plugin) => { const tabs = plugin.tabs; @@ -32,7 +33,7 @@ const getBookmarkTabs = (data) => { }); }); return bookmarkTabs; -}; +}); export const SettingsCenterDropdown = () => { const { snippets = [] } = useACLRoleContext(); const [visible, setVisible] = useState(false); @@ -42,27 +43,28 @@ export const SettingsCenterDropdown = () => { const itemData = useContext(SettingsCenterContext); const pluginsTabs = getPluginsTabs(itemData, snippets); const bookmarkTabs = getBookmarkTabs(pluginsTabs); + const menu = useMemo(() => { + return { + items: [ + ...bookmarkTabs.map((tab) => ({ + key: `/admin/settings/${tab.path}`, + label: compile(tab.title), + })), + { type: 'divider' }, + { + key: '/admin/settings', + label: t('All plugin settings'), + }, + ], + onClick({ key }) { + navigate(key); + }, + }; + }, [bookmarkTabs]); + return ( - ({ - key: `/admin/settings/${tab.path}`, - label: compile(tab.title), - })), - { type: 'divider' }, - { - key: '/admin/settings', - label: t('All plugin settings'), - }, - ], - onClick({ key }) { - navigate(key); - }, - }} - > + ) } - > - {fieldSchema.mapProperties((schema) => { - return ( - - {schema['x-icon'] && } - {schema.title || t('Unnamed')} - - - } - key={schema.name} - /> - ); + items={fieldSchema.mapProperties((schema) => { + return { + label: ( + + {schema['x-icon'] && } + {schema.title || t('Unnamed')} + + + ), + key: schema.name as string, + }; })} - + /> ) } diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index e88b5491a7..9bd5a8a803 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -193,344 +193,347 @@ const useValidator = (validator: (value: any) => string) => { }, []); }; -export const Table: any = observer((props: any) => { - const { pagination: pagination1, useProps, onChange, ...others1 } = props; - const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; - const { - dragSort = false, - showIndex = true, - onRowSelectionChange, - onChange: onTableChange, - rowSelection, - rowKey, - required, - onExpand, - ...others - } = { ...others1, ...others2 } as any; - const field = useArrayField(others); - const columns = useTableColumns(others); - const schema = useFieldSchema(); - const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; - const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); - const { expandFlag } = ctx; - const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); - const paginationProps = usePaginationProps(pagination1, pagination2); - // const requiredValidator = field.required || required; - const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; - const [expandedKeys, setExpandesKeys] = useState([]); - const [allIncludesChildren, setAllIncludesChildren] = useState([]); - const [selectedRowKeys, setSelectedRowKeys] = useState(field?.data?.selectedRowKeys || []); - const [selectedRow, setSelectedRow] = useState([]); - const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || []; - const isRowSelect = rowSelection?.type !== 'none'; +export const Table: any = observer( + (props: any) => { + const { pagination: pagination1, useProps, onChange, ...others1 } = props; + const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; + const { + dragSort = false, + showIndex = true, + onRowSelectionChange, + onChange: onTableChange, + rowSelection, + rowKey, + required, + onExpand, + ...others + } = { ...others1, ...others2 } as any; + const field = useArrayField(others); + const columns = useTableColumns(others); + const schema = useFieldSchema(); + const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider'; + const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext(); + const { expandFlag } = ctx; + const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {})); + const paginationProps = usePaginationProps(pagination1, pagination2); + // const requiredValidator = field.required || required; + const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; + const [expandedKeys, setExpandesKeys] = useState([]); + const [allIncludesChildren, setAllIncludesChildren] = useState([]); + const [selectedRowKeys, setSelectedRowKeys] = useState(field?.data?.selectedRowKeys || []); + const [selectedRow, setSelectedRow] = useState([]); + const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || []; + const isRowSelect = rowSelection?.type !== 'none'; - let onRow = null, - highlightRow = ''; + let onRow = null, + highlightRow = ''; - if (onClickRow) { - onRow = (record) => { - return { - onClick: () => onClickRow(record, setSelectedRow, selectedRow), + if (onClickRow) { + onRow = (record) => { + return { + onClick: () => onClickRow(record, setSelectedRow, selectedRow), + }; }; - }; - highlightRow = css` - & > td { - background-color: #caedff !important; - } - &:hover > td { - background-color: #caedff !important; - } - `; - } - - // useEffect(() => { - // field.setValidator((value) => { - // if (requiredValidator) { - // return Array.isArray(value) && value.length > 0 ? null : 'The field value is required'; - // } - // return; - // }); - // }, [requiredValidator]); - - useEffect(() => { - if (treeTable !== false) { - const keys = getIdsWithChildren(field.value?.slice?.()); - setAllIncludesChildren(keys); + highlightRow = css` + & > td { + background-color: #caedff !important; + } + &:hover > td { + background-color: #caedff !important; + } + `; } - }, [field.value]); - useEffect(() => { - if (expandFlag) { - setExpandesKeys(allIncludesChildren); - } else { - setExpandesKeys([]); - } - }, [expandFlag, allIncludesChildren]); - const components = useMemo(() => { - return { - header: { - wrapper: (props) => { - return ( - - - - ); + // useEffect(() => { + // field.setValidator((value) => { + // if (requiredValidator) { + // return Array.isArray(value) && value.length > 0 ? null : 'The field value is required'; + // } + // return; + // }); + // }, [requiredValidator]); + + useEffect(() => { + if (treeTable !== false) { + const keys = getIdsWithChildren(field.value?.slice?.()); + setAllIncludesChildren(keys); + } + }, [field.value]); + useEffect(() => { + if (expandFlag) { + setExpandesKeys(allIncludesChildren); + } else { + setExpandesKeys([]); + } + }, [expandFlag, allIncludesChildren]); + + const components = useMemo(() => { + return { + header: { + wrapper: (props) => { + return ( + + + + ); + }, + cell: (props) => { + return ( + + ); + }, }, - cell: (props) => { - return ( - { + return ( + { + if (!e.active || !e.over) { + console.warn('move cancel'); + return; + } + + const fromIndex = e.active?.data.current?.sortable?.index; + const toIndex = e.over?.data.current?.sortable?.index; + const from = field.value[fromIndex]; + const to = field.value[toIndex]; + field.move(fromIndex, toIndex); + onRowDragEnd({ fromIndex, toIndex, from, to }); + }} + > + + + ); + }, + row: (props) => { + return ; + }, + cell: (props) => ( + - ); + ), }, - }, - body: { - wrapper: (props) => { - return ( - { - if (!e.active || !e.over) { - console.warn('move cancel'); - return; - } + }; + }, [field, onRowDragEnd, dragSort]); - const fromIndex = e.active?.data.current?.sortable?.index; - const toIndex = e.over?.data.current?.sortable?.index; - const from = field.value[fromIndex]; - const to = field.value[toIndex]; - field.move(fromIndex, toIndex); - onRowDragEnd({ fromIndex, toIndex, from, to }); - }} - > - - - ); - }, - row: (props) => { - return ; - }, - cell: (props) => ( - - ), - }, + const defaultRowKey = (record: any) => { + return field.value?.indexOf?.(record); }; - }, [field, onRowDragEnd, dragSort]); - const defaultRowKey = (record: any) => { - return field.value?.indexOf?.(record); - }; + const getRowKey = (record: any) => { + if (typeof rowKey === 'string') { + return record[rowKey]?.toString(); + } else { + return (rowKey ?? defaultRowKey)(record)?.toString(); + } + }; - const getRowKey = (record: any) => { - if (typeof rowKey === 'string') { - return record[rowKey]?.toString(); - } else { - return (rowKey ?? defaultRowKey)(record)?.toString(); - } - }; - - const restProps = { - rowSelection: rowSelection - ? { - type: 'checkbox', - selectedRowKeys: selectedRowKeys, - onChange(selectedRowKeys: any[], selectedRows: any[]) { - field.data = field.data || {}; - field.data.selectedRowKeys = selectedRowKeys; - setSelectedRowKeys(selectedRowKeys); - onRowSelectionChange?.(selectedRowKeys, selectedRows); - }, - renderCell: (checked, record, index, originNode) => { - if (!dragSort && !showIndex) { - return originNode; - } - const current = props?.pagination?.current; - const pageSize = props?.pagination?.pageSize || 20; - if (current) { - index = index + (current - 1) * pageSize + 1; - } else { - index = index + 1; - } - if (record.__index) { - index = extractIndex(record.__index); - } - return ( -
+ const restProps = { + rowSelection: rowSelection + ? { + type: 'checkbox', + selectedRowKeys: selectedRowKeys, + onChange(selectedRowKeys: any[], selectedRows: any[]) { + field.data = field.data || {}; + field.data.selectedRowKeys = selectedRowKeys; + setSelectedRowKeys(selectedRowKeys); + onRowSelectionChange?.(selectedRowKeys, selectedRows); + }, + renderCell: (checked, record, index, originNode) => { + if (!dragSort && !showIndex) { + return originNode; + } + const current = props?.pagination?.current; + const pageSize = props?.pagination?.pageSize || 20; + if (current) { + index = index + (current - 1) * pageSize + 1; + } else { + index = index + 1; + } + if (record.__index) { + index = extractIndex(record.__index); + } + return (
- {dragSort && } - {showIndex && } -
- {isRowSelect && (
- {originNode} + {dragSort && } + {showIndex && }
- )} -
- ); - }, - ...rowSelection, - } - : undefined, - }; - const SortableWrapper = useCallback( - ({ children }) => { - return dragSort - ? React.createElement(SortableContext, { - items: field.value?.map?.(getRowKey) || [], - children: children, - }) - : React.createElement(React.Fragment, { - children, - }); - }, - [field, dragSort], - ); - const fieldSchema = useFieldSchema(); - const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; + {isRowSelect && ( +
+ {originNode} +
+ )} + + ); + }, + ...rowSelection, + } + : undefined, + }; + const SortableWrapper = useCallback( + ({ children }) => { + return dragSort + ? React.createElement(SortableContext, { + items: field.value?.map?.(getRowKey) || [], + children, + }) + : React.createElement(React.Fragment, { + children, + }); + }, + [field, dragSort], + ); + const fieldSchema = useFieldSchema(); + const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; - const { height: tableHeight, tableSizeRefCallback } = useTableSize(); - const scroll = useMemo(() => { - return fixedBlock - ? { - x: 'max-content', - y: tableHeight, - } - : { - x: 'max-content', - }; - }, [fixedBlock, tableHeight]); - return ( -
{ + return fixedBlock + ? { + x: 'max-content', + y: tableHeight, + } + : { + x: 'max-content', + }; + }, [fixedBlock, tableHeight]); + return ( +
- - { - onTableChange?.(pagination, filters, sorter, extra); - }} - onRow={onRow} - rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} - tableLayout={'auto'} - scroll={scroll} - columns={columns} - expandable={{ - onExpand: (flag, record) => { - const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i); - setExpandesKeys(newKeys); - onExpand?.(flag, record); - }, - expandedRowKeys: expandedKeys, - }} - /> - - {field.errors.length > 0 && ( -
- {field.errors.map((error) => { - return error.messages.map((message) =>
{message}
); - })} -
- )} -
- ); -}); + .ant-table { + overflow-x: auto; + overflow-y: hidden; + } + `} + > + + { + onTableChange?.(pagination, filters, sorter, extra); + }} + onRow={onRow} + rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} + tableLayout={'auto'} + scroll={scroll} + columns={columns} + expandable={{ + onExpand: (flag, record) => { + const newKeys = flag ? [...expandedKeys, record.id] : expandedKeys.filter((i) => record.id !== i); + setExpandesKeys(newKeys); + onExpand?.(flag, record); + }, + expandedRowKeys: expandedKeys, + }} + /> + + {field.errors.length > 0 && ( +
+ {field.errors.map((error) => { + return error.messages.map((message) =>
{message}
); + })} +
+ )} +
+ ); + }, + { displayName: 'Table' }, +); diff --git a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx index 64e0e7aec2..f2e9adec7f 100644 --- a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx +++ b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx @@ -1,15 +1,15 @@ import { css } from '@emotion/css'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; -import { TabPaneProps, Tabs as AntdTabs, TabsProps } from 'antd'; +import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd'; import classNames from 'classnames'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Icon } from '../../../icon'; import { useSchemaInitializer } from '../../../schema-initializer'; import { DndContext, SortableItem } from '../../common'; +import { useDesignable } from '../../hooks'; import { useDesigner } from '../../hooks/useDesigner'; import { useTabsContext } from './context'; import { TabsDesigner } from './Tabs.Designer'; -import { useDesignable } from '../../hooks'; export const Tabs: any = observer( (props: TabsProps) => { @@ -19,20 +19,33 @@ export const Tabs: any = observer( const contextProps = useTabsContext(); const { PaneRoot = React.Fragment as React.FC } = contextProps; + const items = useMemo(() => { + const result = fieldSchema.mapProperties((schema, key: string) => { + return { + key, + label: , + children: ( + + + + ), + }; + }); + + if (designable) { + result.push({ + key: 'designer', + label: render(), + children: null, + }); + } + + return result; + }, [fieldSchema.mapProperties((s, key) => key).join()]); + return ( - - {fieldSchema.mapProperties((schema, key) => { - return ( - } key={key}> - - - - - ); - })} - {designable && } - + ); }, diff --git a/packages/core/client/src/schema-component/antd/upload/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/upload/ReadPretty.tsx index f5841591dd..a44f508514 100644 --- a/packages/core/client/src/schema-component/antd/upload/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/upload/ReadPretty.tsx @@ -18,7 +18,7 @@ type Composed = React.FC & { export const ReadPretty: Composed = () => null; -ReadPretty.File = (props: UploadProps) => { +ReadPretty.File = function File(props: UploadProps) { const record = useRecord(); const field = useField(); const value = isString(field.value) ? record : field.value; @@ -44,7 +44,7 @@ ReadPretty.File = (props: UploadProps) => { // } }; return ( -
+
@@ -114,6 +114,7 @@ ReadPretty.File = (props: UploadProps) => { imageTitle={images[photoIndex]?.title} toolbarButtons={[ + )} +
+ ); + if (!shouldRender || !items.length) { + return buttonDom; + } + const insertSchema = (schema) => { - if (props.insert) { - props.insert(wrap(schema)); + if (insert) { + insert(wrap(schema)); } else { insertAdjacent(insertPosition, wrap(schema), { onSuccess }); } }; + const renderItems = (items: any) => { return items - .filter((v) => { + .filter((v: any) => { return v && (v?.visible ? v.visible() : true); }) - ?.map((item, indexA) => { + ?.map((item: any, indexA: number) => { if (item.type === 'divider') { - return ; + return { type: 'divider', key: item.key || `item-${indexA}` }; } if (item.type === 'item' && item.component) { const Component = findComponent(item.component); - item.key = `${item.key || item.title}-${indexA}`; - return ( - Component && ( + if (!Component) { + error(`SchemaInitializer: component "${item.component}" not found`); + return null; + } + if (!item.key) { + item.key = `${item.title}-${indexA}`; + } + return getMenuItem(() => { + return ( - ) - ); + ); + }); } if (item.type === 'itemGroup') { + const label = compile(item.title); return ( - !!item.children?.length && ( - - {renderItems(item.children)} - - ) + !!item.children?.length && { + type: 'group', + key: item.key || `item-group-${indexA}`, + label, + title: label, + children: renderItems(item.children), + } ); } if (item.type === 'subMenu') { + const label = compile(item.title); return ( - !!item.children?.length && ( - - {renderItems(item.children)} - - ) + !!item.children?.length && { + key: item.key || `item-group-${indexA}`, + label, + title: label, + popupClassName: menuItemGroupCss, + children: renderItems(item.children), + } ); } }); }; - const buttonDom = ( - - ); - if (!items.length) { - return buttonDom; - } - const menu = {renderItems(items)}; - if (!designable && props.designable !== true) { - return null; - } + clean(); + const menuItems = renderItems(items); + return ( - + + { - setVisible(visible); + onOpenChange={() => { + // 如果不清空输入框的值,那么下次打开的时候会出现上次输入的值 + setSearchValue(''); + setShouldRender(false); + setVisible(false); + }} + menu={{ + style: { + maxHeight: '60vh', + overflowY: 'auto', + }, + items: menuItems, }} {...dropdown} - overlay={menu} > {component ? component : buttonDom} @@ -158,10 +203,17 @@ SchemaInitializer.Button = observer( { displayName: 'SchemaInitializer.Button' }, ); -SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { - const { index, info } = useContext(SchemaInitializerItemContext); +SchemaInitializer.Item = function Item(props: SchemaInitializerItemProps) { + const { info } = useContext(SchemaInitializerItemContext); const compile = useCompile(); - const { eventKey, items = [], children = info?.title, icon, onClick, ...others } = props; + const { items = [], children = info?.title, icon, onClick } = props; + const { collectMenuItem } = useCollectMenuItem(); + + if (!collectMenuItem) { + error('SchemaInitializer.Item: collectMenuItem is undefined, please check the context'); + return null; + } + if (items?.length > 0) { const renderMenuItem = (items: SchemaInitializerItemOptions[]) => { if (!items?.length) { @@ -169,77 +221,70 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => { } return items.map((item, indexA) => { if (item.type === 'divider') { - return ; + return { type: 'divider', key: `divider-${indexA}` }; } if (item.type === 'itemGroup') { - return ( - - {renderMenuItem(item.children)} - - ); + const label = compile(item.title); + return { + type: 'group', + key: item.key || `item-group-${indexA}`, + label, + title: label, + className: menuItemGroupCss, + children: renderMenuItem(item.children), + } as MenuProps['items'][0]; } if (item.type === 'subMenu') { - return ( - - {renderMenuItem(item.children)} - - ); + const label = compile(item.title); + return { + key: item.key || `sub-menu-${indexA}`, + label, + title: label, + children: renderMenuItem(item.children), + }; } - return ( - { - item?.clearKeywords?.(); - if (item.onClick) { - item.onClick({ ...info, item }); - } else { - onClick({ ...info, item }); - } - }} - > - {compile(item.title)} - - ); + const label = compile(item.title); + return { + key: item.key || `${info.key}-${item.title}-${indexA}`, + label, + title: label, + onClick: (info) => { + item?.clearKeywords?.(); + if (item.onClick) { + item.onClick({ ...info, item }); + } else { + onClick({ ...info, item }); + } + }, + }; }); }; - return ( - : icon} - > - {renderMenuItem(items)} - - ); + + const item = { + key: info.key, + label: isString(children) ? compile(children) : children, + icon: typeof icon === 'string' ? : icon, + children: renderMenuItem(items), + }; + + collectMenuItem(item); + return null; } - return ( - : icon} - onClick={(opts) => { - info?.clearKeywords?.(); - onClick({ ...opts, item: info }); - }} - > - {compile(children)} - - ); + + const label = isString(children) ? compile(children) : children; + const item = { + key: info.key, + label, + title: label, + icon: typeof icon === 'string' ? : icon, + onClick: (opts) => { + info?.clearKeywords?.(); + onClick({ ...opts, item: info }); + }, + }; + + collectMenuItem(item); + return null; }; SchemaInitializer.itemWrap = (component?: SchemaInitializerItemComponent) => { @@ -253,11 +298,13 @@ interface SchemaInitializerActionModalProps { onSubmit?: (values: any) => void; buttonText?: any; } -SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => { +SchemaInitializer.ActionModal = function ActionModal(props: SchemaInitializerActionModalProps) { const { title, schema, buttonText, 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() { @@ -269,7 +316,9 @@ SchemaInitializer.ActionModal = (props: SchemaInitializerActionModalProps) => { }, [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() { diff --git a/packages/core/client/src/schema-initializer/SelectCollection.tsx b/packages/core/client/src/schema-initializer/SelectCollection.tsx index 3764231a4d..f669b1a991 100644 --- a/packages/core/client/src/schema-initializer/SelectCollection.tsx +++ b/packages/core/client/src/schema-initializer/SelectCollection.tsx @@ -1,31 +1,28 @@ import { Divider, Input } from 'antd'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionManager } from '../collection-manager'; -export const SelectCollection = ({ value, onChange, setSelected }) => { +export const SelectCollection = ({ value: outValue, onChange }) => { const { t } = useTranslation(); - const { collections } = useCollectionManager(); + const [value, setValue] = useState(outValue); + + // 之所以要增加个内部的 value 是为了防止用户输入过快时造成卡顿的问题 + useEffect(() => { + setValue(outValue); + }, [outValue]); return (
{ - const names = collections - .filter((collection) => { - if (!collection.title) { - return; - } - return collection.title.toUpperCase().includes(e.target.value.toUpperCase()); - }) - .map((item) => item.name); - setSelected(names); onChange(e.target.value); + setValue(e.target.value); }} /> diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx index dea3b2e274..4d52054780 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionColumnInitializers.tsx @@ -1,12 +1,12 @@ import { MenuOutlined } from '@ant-design/icons'; 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 { useCollection } from '../../collection-manager'; import { createDesignable, useDesignable } from '../../schema-component'; -import _ from 'lodash'; export const Resizable = (props) => { const { t } = useTranslation(); diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx index 1aea86da3f..6b79ab929c 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -32,7 +32,7 @@ export const TableActionInitializers = { skipScopeCheck: true, }, }, - visible: () => { + visible: function useVisible() { const collection = useCollection(); return collection.template !== 'view' && collection.template !== 'file'; }, @@ -45,7 +45,7 @@ export const TableActionInitializers = { 'x-align': 'right', 'x-decorator': 'ACLActionProvider', }, - visible: () => { + visible: function useVisible() { const collection = useCollection(); return (collection as any).template !== 'view'; }, @@ -65,7 +65,7 @@ export const TableActionInitializers = { schema: { 'x-align': 'right', }, - visible: () => { + visible: function useVisible() { const schema = useFieldSchema(); const collection = useCollection(); const { treeTable } = schema?.parent?.['x-decorator-props'] || {}; @@ -76,7 +76,7 @@ export const TableActionInitializers = { }, { type: 'divider', - visible: () => { + visible: function useVisible() { const collection = useCollection(); return (collection as any).template !== 'view'; }, @@ -157,7 +157,7 @@ export const TableActionInitializers = { }, }, ], - visible: () => { + visible: function useVisible() { const collection = useCollection(); return (collection as any).template !== 'view'; }, diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 490fe0fada..540df7b890 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -1,8 +1,8 @@ import { DownOutlined, PlusOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; import { RecursionField, observer, useField, useFieldSchema } from '@formily/react'; -import { Button, Dropdown, Menu } from 'antd'; -import React, { useEffect, useState } from 'react'; +import { Button, Dropdown, MenuProps } from 'antd'; +import React, { useEffect, useMemo, useState } from 'react'; import { useDesignable } from '../../'; import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider'; import { CollectionProvider, useCollection, useCollectionManager } from '../../collection-manager'; @@ -131,44 +131,44 @@ export const CreateAction = observer( const componentType = field.componentProps.type || 'primary'; const { getChildrenCollections } = useCollectionManager(); const totalChildCollections = getChildrenCollections(collection.name); - const inheritsCollections = enableChildren - .map((k) => { - if (!k) { - return; - } - const childCollection = totalChildCollections.find((j) => j.name === k.collection); - if (!childCollection) { - return; - } - return { - ...childCollection, - title: k.title || childCollection.title, - }; - }) - .filter((v) => { - return v && actionAclCheck(`${v.name}:create`); - }); + const inheritsCollections = useMemo(() => { + return enableChildren + .map((k) => { + if (!k) { + return; + } + const childCollection = totalChildCollections.find((j) => j.name === k.collection); + if (!childCollection) { + return; + } + return { + ...childCollection, + title: k.title || childCollection.title, + }; + }) + .filter((v) => { + return v && actionAclCheck(`${v.name}:create`); + }); + }, [enableChildren, totalChildCollections]); const linkageRules = fieldSchema?.['x-linkage-rules'] || []; const values = useRecord(); const compile = useCompile(); const { designable } = useDesignable(); const icon = props.icon || ; - const menu = ( - - {inheritsCollections.map((option) => { - return ( - { - onClick?.(option.name); - }} - > - {compile(option.title)} - - ); - })} - - ); + const menuItems = useMemo(() => { + return inheritsCollections.map((option) => ({ + key: option.name, + label: compile(option.title), + onClick: () => onClick?.(option.name), + })); + }, [inheritsCollections, onClick]); + + const menu = useMemo(() => { + return { + items: menuItems, + }; + }, [menuItems]); + useEffect(() => { field.linkageProperty = {}; linkageRules @@ -190,7 +190,7 @@ export const CreateAction = observer( leftButton, React.cloneElement(rightButton as React.ReactElement, { loading: false }), ]} - overlay={menu} + menu={menu} onClick={(info) => { onClick?.(collection.name); }} @@ -199,7 +199,7 @@ export const CreateAction = observer( {props.children} ) : ( - + {