diff --git a/CHANGELOG.md b/CHANGELOG.md index 958e0d3e98..3f2ee38c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14 + +### 🎉 New Features + +- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos + +### 🐛 Bug Fixes + +- **[client]** + - The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe + + - "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh + + - In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe + + - Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe + +- **[File storage: S3(Pro)]** + - Organize language by @jiannx + + - Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx + +- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust + ## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 ### 🐛 Bug Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 933a774f55..7bb43bdfb4 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,30 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14 + +### 🎉 新特性 + +- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos + +### 🐛 修复 + +- **[client]** + - 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe + + - 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh + + - 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe + + - 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe + +- **[文件存储:S3 (Pro)]** + - 整理语言文案 by @jiannx + + - baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx + +- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust + ## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 ### 🐛 修复 diff --git a/docker/nocobase/Dockerfile b/docker/nocobase/Dockerfile index 5de0d6030a..3fb6bafc89 100644 --- a/docker/nocobase/Dockerfile +++ b/docker/nocobase/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app RUN cd /app \ && yarn config set network-timeout 600000 -g \ - && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app -a -e APP_ENV=production \ + && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app --skip-dev-dependencies -a -e APP_ENV=production \ && cd /app/my-nocobase-app \ && yarn install --production diff --git a/lerna.json b/lerna.json index c8bf5a4ca6..caa46b0f70 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "npmClient": "yarn", "useWorkspaces": true, "npmClientArgs": ["--ignore-engines"], diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json index d9f7aa3c5b..20ca10f80e 100644 --- a/packages/core/acl/package.json +++ b/packages/core/acl/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/acl", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/resourcer": "1.7.0-beta.16", - "@nocobase/utils": "1.7.0-beta.16", + "@nocobase/resourcer": "1.7.0-beta.18", + "@nocobase/utils": "1.7.0-beta.18", "minimatch": "^5.1.1" }, "repository": { diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index 351a6ff2c1..81ae052a0f 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -1,14 +1,14 @@ { "name": "@nocobase/actions", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/cache": "1.7.0-beta.16", - "@nocobase/database": "1.7.0-beta.16", - "@nocobase/resourcer": "1.7.0-beta.16" + "@nocobase/cache": "1.7.0-beta.18", + "@nocobase/database": "1.7.0-beta.18", + "@nocobase/resourcer": "1.7.0-beta.18" }, "repository": { "type": "git", diff --git a/packages/core/app/package.json b/packages/core/app/package.json index 4fa74d2f60..0e849790ec 100644 --- a/packages/core/app/package.json +++ b/packages/core/app/package.json @@ -1,17 +1,17 @@ { "name": "@nocobase/app", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/database": "1.7.0-beta.16", - "@nocobase/preset-nocobase": "1.7.0-beta.16", - "@nocobase/server": "1.7.0-beta.16" + "@nocobase/database": "1.7.0-beta.18", + "@nocobase/preset-nocobase": "1.7.0-beta.18", + "@nocobase/server": "1.7.0-beta.18" }, "devDependencies": { - "@nocobase/client": "1.7.0-beta.16" + "@nocobase/client": "1.7.0-beta.18" }, "repository": { "type": "git", diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index 17abe7e5ca..e78fbe7da0 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -1,16 +1,16 @@ { "name": "@nocobase/auth", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.7.0-beta.16", - "@nocobase/cache": "1.7.0-beta.16", - "@nocobase/database": "1.7.0-beta.16", - "@nocobase/resourcer": "1.7.0-beta.16", - "@nocobase/utils": "1.7.0-beta.16", + "@nocobase/actions": "1.7.0-beta.18", + "@nocobase/cache": "1.7.0-beta.18", + "@nocobase/database": "1.7.0-beta.18", + "@nocobase/resourcer": "1.7.0-beta.18", + "@nocobase/utils": "1.7.0-beta.18", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" }, diff --git a/packages/core/build/package.json b/packages/core/build/package.json index b27f603065..06aa9f32f5 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/build", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "Library build tool based on rollup.", "main": "lib/index.js", "types": "./lib/index.d.ts", @@ -17,7 +17,7 @@ "@lerna/project": "4.0.0", "@rsbuild/plugin-babel": "^1.0.3", "@rsdoctor/rspack-plugin": "^0.4.8", - "@rspack/core": "1.1.1", + "@rspack/core": "1.3.2", "@svgr/webpack": "^8.1.0", "@types/gulp": "^4.0.13", "@types/lerna__package": "5.1.0", diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts index f851f37ed8..9b7f228072 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou umdNamedDefine: true, }, }, + amd: {}, resolve: { tsConfig: path.join(process.cwd(), 'tsconfig.json'), extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'], diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json index 55c0b2dda2..13372d0c7f 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -1,12 +1,12 @@ { "name": "@nocobase/cache", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/lock-manager": "1.7.0-beta.16", + "@nocobase/lock-manager": "1.7.0-beta.18", "bloom-filters": "^3.0.1", "cache-manager": "^5.2.4", "cache-manager-redis-yet": "^4.1.2" diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index ac74694593..ce7485f885 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cli", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", @@ -8,7 +8,7 @@ "nocobase": "./bin/index.js" }, "dependencies": { - "@nocobase/app": "1.7.0-beta.16", + "@nocobase/app": "1.7.0-beta.18", "@types/fs-extra": "^11.0.1", "@umijs/utils": "3.5.20", "chalk": "^4.1.1", @@ -18,14 +18,13 @@ "fast-glob": "^3.3.1", "fs-extra": "^11.1.1", "p-all": "3.0.0", - "pm2": "^5.2.0", + "pm2": "^6.0.5", "portfinder": "^1.0.28", - "serve": "^13.0.2", "tree-kill": "^1.2.2", "tsx": "^4.19.0" }, "devDependencies": { - "@nocobase/devtools": "1.7.0-beta.16" + "@nocobase/devtools": "1.7.0-beta.18" }, "repository": { "type": "git", diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 35b496db1f..1c760cd4ba 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -460,8 +460,16 @@ exports.initEnv = function initEnv() { process.env.SOCKET_PATH = generateGatewayPath(); fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true }); fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true }); - const pkgDir = resolve(process.cwd(), 'storage/plugins', '@nocobase/plugin-multi-app-manager'); - fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true }); + const pkgs = [ + '@nocobase/plugin-multi-app-manager', + '@nocobase/plugin-departments', + '@nocobase/plugin-field-attachment-url', + '@nocobase/plugin-workflow-response-message', + ]; + for (const pkg of pkgs) { + const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg); + fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true }); + } }; exports.generatePlugins = function () { diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index 1debccd479..8389585d18 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -234,6 +234,10 @@ export default defineConfig({ "title": "Filter", "link": "/components/filter" }, + { + "title": "LinkageFilter", + "link": "/components/linkage-filter" + }, ] }, { diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 792f210811..bb51b5c8ef 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/client", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.18", "license": "AGPL-3.0", "main": "lib/index.js", "module": "es/index.mjs", @@ -27,9 +27,9 @@ "@formily/reactive-react": "^2.2.27", "@formily/shared": "^2.2.27", "@formily/validator": "^2.2.27", - "@nocobase/evaluators": "1.7.0-beta.16", - "@nocobase/sdk": "1.7.0-beta.16", - "@nocobase/utils": "1.7.0-beta.16", + "@nocobase/evaluators": "1.7.0-beta.18", + "@nocobase/sdk": "1.7.0-beta.18", + "@nocobase/utils": "1.7.0-beta.18", "ahooks": "^3.7.2", "antd": "5.24.2", "antd-style": "3.7.1", diff --git a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx index f6eacc4ede..1a46420a4b 100644 --- a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx @@ -10,11 +10,11 @@ import { useFieldSchema } from '@formily/react'; import React from 'react'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; -import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component'; +import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField'; +import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component'; import { DefaultValueProvider } from '../schema-settings'; import { CollectOperators } from './CollectOperators'; import { FormBlockProvider } from './FormBlockProvider'; -import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField'; export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { const filedSchema = useFieldSchema(); @@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { }} > false}> - + diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 9dfead7b02..d34a8d540e 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -546,9 +546,11 @@ export const useFilterBlockActionProps = () => { const { doFilter } = useDoFilter(); const actionField = useField(); actionField.data = actionField.data || {}; + const form = useForm(); return { async onClick() { + await form.submit(); actionField.data.loading = true; await doFilter(); actionField.data.loading = false; @@ -1580,7 +1582,7 @@ export const getAppends = ({ const fieldNames = getTargetField(item); // 只应该收集关系字段,只有大于 1 的时候才是关系字段 - if (fieldNames.length > 1) { + if (fieldNames.length > 1 && !item.op) { appends.add(fieldNames.join('.')); } }); diff --git a/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx index f0e79b3eb7..9c4d55b34a 100644 --- a/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx +++ b/packages/core/client/src/modules/actions/add-new/addNewActionSettings.tsx @@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings'; import { useOpenModeContext } from '../../popup/OpenModeProvider'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; +import { useDataBlockProps } from '../../../data-source'; export const addNewActionSettings = new SchemaSettings({ name: 'actionSettings:addNew', @@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({ return buttonEditorProps; }, }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + }; + }, + }, { name: 'openMode', Component: SchemaSettingOpenModeSchemaItems, diff --git a/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx index be35280a42..f5c2ab2409 100644 --- a/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx +++ b/packages/core/client/src/modules/actions/bulk-destroy/bulkDeleteActionSettings.tsx @@ -15,6 +15,7 @@ import { SecondConFirm, RefreshDataBlockRequest, } from '../../../schema-component/antd/action/Action.Designer'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; export const bulkDeleteActionSettings = new SchemaSettings({ name: 'actionSettings:bulkDelete', @@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({ return buttonEditorProps; }, }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + }; + }, + }, { name: 'secondConFirm', Component: SecondConFirm, diff --git a/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx b/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx index bfffdcbee2..1708fbc8ad 100644 --- a/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx +++ b/packages/core/client/src/modules/actions/disassociate/disassociateActionSettings.tsx @@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({ name: 'linkageRules', Component: SchemaSettingsLinkageRules, useComponentProps() { - const { name } = useCollection_deprecated(); const { linkageRulesProps } = useSchemaToolbar(); return { ...linkageRulesProps, - collectionName: name, }; }, }, diff --git a/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx index be6e5a8c9d..d8b5f45f79 100644 --- a/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx +++ b/packages/core/client/src/modules/actions/expand-collapse/expendableActionSettings.tsx @@ -14,7 +14,7 @@ import { useDesignable } from '../../..'; import { useSchemaToolbar } from '../../../application'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; -import { SchemaSettingsModalItem } from '../../../schema-settings'; +import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings'; function ButtonEditor() { const field = useField(); @@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({ return buttonEditorProps; }, }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { linkageRulesProps } = useSchemaToolbar(); + + return { + ...linkageRulesProps, + }; + }, + }, { name: 'remove', sort: 100, diff --git a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx index c5a0cb87c8..34c51a0556 100644 --- a/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx +++ b/packages/core/client/src/modules/actions/link/customizeLinkActionSettings.tsx @@ -11,10 +11,9 @@ import { useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { useCollectionRecord, useDesignable } from '../../../'; +import { useDesignable } from '../../../'; import { useSchemaToolbar } from '../../../application'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; -import { useCollection_deprecated } from '../../../collection-manager'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { SchemaSettingsLinkageRules, @@ -22,6 +21,7 @@ import { SchemaSettingAccessControl, } from '../../../schema-settings'; import { useURLAndHTMLSchema } from './useURLAndHTMLSchema'; +import { useDataBlockProps } from '../../../data-source'; export const SchemaSettingsActionLinkItem: FC = () => { const field = useField(); @@ -94,16 +94,10 @@ export const customizeLinkActionSettings = new SchemaSettings({ { name: 'linkageRules', Component: SchemaSettingsLinkageRules, - useVisible() { - const record = useCollectionRecord(); - return !_.isEmpty(record?.data); - }, useComponentProps() { - const { name } = useCollection_deprecated(); const { linkageRulesProps } = useSchemaToolbar(); return { ...linkageRulesProps, - collectionName: name, }; }, }, diff --git a/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx index b8b31acada..cb7c94ecb7 100644 --- a/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx +++ b/packages/core/client/src/modules/actions/refresh/refreshActionSettings.tsx @@ -10,7 +10,7 @@ import { useSchemaToolbar } from '../../../application'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; - +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; export const refreshActionSettings = new SchemaSettings({ name: 'actionSettings:refresh', items: [ @@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({ return buttonEditorProps; }, }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { linkageRulesProps } = useSchemaToolbar(); + + return { + ...linkageRulesProps, + }; + }, + }, { name: 'secondConFirm', Component: SecondConFirm, diff --git a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx index 34bcb7cdef..0c48ebfc7d 100644 --- a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx @@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; import { useDataBlockProps } from '../../../data-source'; +import { SchemaSettingsLinkageRules } from '../../../schema-settings'; const Tree = connect( AntdTree, @@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({ return buttonEditorProps; }, }, + { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const { linkageRulesProps } = useSchemaToolbar(); + return { + ...linkageRulesProps, + }; + }, + }, { name: 'secondConfirmation', Component: SecondConFirm, diff --git a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx index 560785611e..f1441344b4 100644 --- a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx @@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({ collectionName: name, }; }, - useVisible() { - const fieldSchema = useFieldSchema(); - return !fieldSchema.parent['x-initializer'].includes('bulkEditForm'); - }, }, { name: 'secondConfirmation', diff --git a/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx index 4e86e008d8..dbec03441c 100644 --- a/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx +++ b/packages/core/client/src/modules/actions/view-edit-popup/customizePopupActionSettings.tsx @@ -6,17 +6,12 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ - -import { useFieldSchema } from '@formily/react'; import { useSchemaToolbar } from '../../../application'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; -import { useCollection_deprecated } from '../../../collection-manager'; -import { useCollection } from '../../../data-source'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings'; import { useOpenModeContext } from '../../popup/OpenModeProvider'; -import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; export const customizePopupActionSettings = new SchemaSettings({ name: 'actionSettings:popup', @@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({ name: 'linkageRules', Component: SchemaSettingsLinkageRules, useComponentProps() { - const { name } = useCollection_deprecated(); const { linkageRulesProps } = useSchemaToolbar(); return { ...linkageRulesProps, - collectionName: name, }; }, - useVisible() { - const { collection } = useCurrentPopupRecord() || {}; - const currentCollection = useCollection(); - return !collection || collection?.name === currentCollection?.name; - }, }, { name: 'openMode', diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts index 7550324146..bc0708f2b6 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts @@ -35,11 +35,16 @@ test.describe('linkage rules', () => { // 条件:singleLineText 字段的值包含 123 时 await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByTestId('select-filter-field').click(); - await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click(); - await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').click(); - await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').fill('123'); + await page.getByLabel('variable-button').first().click(); + await page.getByText('Current form').last().click(); + await page.getByText('Current form').last().click(); + await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click(); + + // await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click(); + await page.getByTestId('right-filter-field').getByRole('textbox').click(); + await page.getByTestId('right-filter-field').getByRole('textbox').fill('123'); + await page.getByRole('tabpanel').getByRole('textbox').last().fill('123'); // action:禁用 longText 字段 await page.getByText('Add property').click(); await page.getByTestId('select-linkage-property-field').click(); @@ -81,7 +86,7 @@ test.describe('linkage rules', () => { // 修改第一组规则,使其条件中包含一个变量 -------------------------------------------------------------------------- // 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段 await openLinkageRules(); - await page.getByLabel('variable-button').click(); + await page.getByLabel('variable-button').last().click(); await expectSupportedVariables(page, [ 'Constant', 'Current user', @@ -136,8 +141,13 @@ test.describe('linkage rules', () => { .getByText('Add condition', { exact: true }) .last() .click(); - await page.getByRole('button', { name: 'Select field' }).click(); - await page.getByRole('menuitemcheckbox', { name: 'number' }).click(); + // await page.getByRole('button', { name: 'Select field' }).click(); + + await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click(); + await page.getByText('Current form').last().click(); + await page.getByText('Current form').last().click(); + await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click(); + await page.getByLabel('Linkage rules').getByRole('spinbutton').click(); await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123'); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts index 25c80340d2..1461e81c7e 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/deprecatedVariables.test.ts @@ -22,7 +22,7 @@ test.describe('deprecated variables', () => { await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible(); // 2. 但是变量列表中是禁用状态 - await page.locator('button').filter({ hasText: /^x$/ }).click(); + await page.locator('button').filter({ hasText: /^x$/ }).last().click(); await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } }); await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass( @@ -45,11 +45,11 @@ test.describe('deprecated variables', () => { await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); // 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示 - await page.locator('button').filter({ hasText: /^x$/ }).click(); + await page.locator('button').filter({ hasText: /^x$/ }).last().click(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1); await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); - await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible(); + await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible(); // 清空表达式 await page.getByLabel('textbox').clear(); await page.getByRole('button', { name: 'OK', exact: true }).click(); @@ -58,7 +58,7 @@ test.describe('deprecated variables', () => { await page.getByLabel('block-item-CardItem-users-form').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); - await page.locator('button').filter({ hasText: /^x$/ }).click(); + await page.locator('button').filter({ hasText: /^x$/ }).last().click(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden(); // 使下拉菜单消失 await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index 7db78e37dc..b137ecea4f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -86,7 +86,6 @@ test.describe('configure fields', () => { await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.mouse.move(600, 0); await page.reload(); - await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( `manyToOne1:${record.manyToOne1.id}`, ); diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts index 506094cbc0..310774c18b 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts @@ -42,6 +42,7 @@ test.describe('where grid card block can be added', () => { await page.getByLabel('schema-initializer-Grid-').nth(1).hover(); await page.getByRole('menuitem', { name: 'Role name' }).click(); await page.mouse.move(300, 0); + await page.reload(); await expect(page.getByText('Root')).toBeVisible(); await expect(page.getByText('Admin')).toBeVisible(); await expect(page.getByText('Member')).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts index 8c2a24c863..7ae4fa7a95 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts @@ -30,7 +30,6 @@ test('action linkage by row data', async ({ page, mockPage }) => { // 添加其他你需要的样式属性 }; }); - expect(adminEditActionStyle.opacity).not.toBe('0.1'); expect(rootEditActionStyle.opacity).not.toBe('1'); }); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts index f51e03f878..ed1dda956b 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaSettings.test.ts @@ -316,7 +316,8 @@ test.describe('actions schema settings', () => { // 添加一个条件:ID 等于 1 await page.getByText('Add condition', { exact: true }).click(); - await page.getByTestId('select-filter-field').click(); + await page.getByTestId('left-filter-field').getByLabel('variable-button').click(); + await page.getByText('Current record').last().click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').fill('1'); @@ -340,7 +341,8 @@ test.describe('actions schema settings', () => { // 添加一个条件:ID 等于 1 await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click(); - await page.getByRole('button', { name: 'Select field' }).click(); + await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click(); + await page.getByText('Current record').last().click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').fill('1'); @@ -902,7 +904,6 @@ test.describe('actions schema settings', () => { await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.mouse.move(300, 0); await page.getByRole('button', { name: 'Submit' }).click(); - await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover(); await page.getByRole('menuitem', { name: 'Tree table' }).click(); @@ -928,6 +929,7 @@ test.describe('actions schema settings', () => { await page.getByLabel('schema-initializer-Grid-form:').hover(); await page.getByRole('menuitem', { name: 'Parent', exact: true }).click(); await page.mouse.move(300, 0); + await page.reload(); await expect( page .getByLabel('block-item-CollectionField-') diff --git a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts index 0684ebf21a..5de86a8bd6 100644 --- a/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts +++ b/packages/core/client/src/modules/fields/component/Select/__e2e__/selectOptionsInLinkageRule.test.ts @@ -27,7 +27,8 @@ test.describe('options of Select field in linkage rule', () => { await page.getByRole('switch', { name: 'On Off' }).click(); await page.getByRole('button', { name: 'OK' }).click(); await page.reload(); - await expect(page.getByRole('option', { name: 'option2' })).toBeVisible(); + await page.getByLabel('block-item-CollectionField-').click(); + await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible(); await expect(page.getByRole('option', { name: 'option3' })).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts index 5efa1553bc..01be31da31 100644 --- a/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/schemaInitializer1.test.ts @@ -215,7 +215,7 @@ test.describe('where to open a popup and what can be added to it', () => { await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); - await page.getByRole('menuitem', { name: 'Table right' }).hover(); + await page.getByRole('menuitem', { name: 'Table right' }).click(); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'One to many' }).click(); @@ -282,7 +282,7 @@ test.describe('where to open a popup and what can be added to it', () => { await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); - await page.getByRole('menuitem', { name: 'Table right' }).hover(); + await page.getByRole('menuitem', { name: 'Table right' }).click(); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'One to many' }).click(); diff --git a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts index 55d755cb8d..7768b268c0 100644 --- a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts +++ b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts @@ -18,7 +18,7 @@ test.describe('variables', () => { await page.getByLabel('action-Action.Link-View-view-').hover(); await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); - await page.getByLabel('variable-button').click(); + await page.getByTestId('left-filter-field').getByLabel('variable-button').click(); // 2. 断言应该显示的变量 ['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach( diff --git a/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts b/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts index 01a1256d2c..59f2f00f24 100644 --- a/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts +++ b/packages/core/client/src/modules/variable/__e2e__/currentRecord.test.ts @@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => { await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByLabel('variable-button').click(); + await page.getByLabel('variable-button').first().click(); // 当前表单中应该包含 “Nickname” 字段 await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); // 当前对象中应该包含 “Role UID” 字段 - await page.getByLabel('variable-button').click(); + await page.getByLabel('variable-button').first().click(); await page.getByText('Current object').click(); await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click(); @@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => { await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByText('Add condition', { exact: true }).click(); - await page.getByLabel('variable-button').click(); + await page.getByLabel('variable-button').first().click(); // 当前记录中应该包含 “Nickname” 字段 await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); - await page.getByLabel('variable-button').click(); + await page.getByLabel('variable-button').first().click(); // 当前对象中应该包含 “Role UID” 字段 await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx new file mode 100644 index 0000000000..5bfd4554d1 --- /dev/null +++ b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx @@ -0,0 +1,212 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { findFirstPageRoute, NocoBaseDesktopRouteType } from '..'; +import { NocoBaseDesktopRoute } from '../convertRoutesToSchema'; + +describe('findFirstPageRoute', () => { + // 基本测试:空路由数组 + it('should return undefined for empty routes array', () => { + const result = findFirstPageRoute([]); + expect(result).toBeUndefined(); + }); + + // 基本测试:undefined 路由数组 + it('should return undefined for undefined routes', () => { + const result = findFirstPageRoute(undefined); + expect(result).toBeUndefined(); + }); + + // 测试:只有一个页面路由 + it('should find the first page route when there is only one page', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0]); + }); + + // 测试:多个页面路由 + it('should find the first page route when there are multiple pages', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + { + id: 2, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0]); + }); + + // 测试:不同类型的路由混合 + it('should find the first page route among mixed route types', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'link1', + type: NocoBaseDesktopRouteType.link, + title: 'Link 1', + }, + { + id: 2, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); + + // 测试:隐藏的菜单项 + it('should ignore hidden menu items', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + hideInMenu: true, + }, + { + id: 2, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); + + // 测试:嵌套路由 + it('should find page route in nested group', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + children: [ + { + id: 11, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0].children[0]); + }); + + // 测试:多层嵌套路由 + it('should find page route in deeply nested groups', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + children: [ + { + id: 11, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1-1', + children: [ + { + id: 111, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0].children[0].children[0]); + }); + + // 测试:复杂路由结构 + it('should find the first visible page in a complex route structure', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + hideInMenu: true, + children: [ + { + id: 11, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + { + id: 2, + type: NocoBaseDesktopRouteType.group, + title: 'Group 2', + children: [ + { + id: 21, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1].children[0]); + }); + + // 测试:空组 + it('should skip empty groups and find page in next group', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Empty Group', + children: [], + }, + { + id: 2, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); +}); 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 d645ed853b..692c9e3f0c 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 @@ -710,27 +710,11 @@ export const InternalAdminLayout = () => { ); }; -function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) { - // Find the first route of type "page" - for (const route of routes) { - if (route.type === NocoBaseDesktopRouteType.page) { - return route.schemaUid; - } - - if (route.children?.length) { - const result = getDefaultPageUid(route.children); - if (result) { - return result; - } - } - } -} - const NavigateToDefaultPage: FC = (props) => { const { allAccessRoutes } = useAllAccessDesktopRoutes(); const location = useLocationNoUpdate(); - const defaultPageUid = getDefaultPageUid(allAccessRoutes); + const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid; return ( <> @@ -962,16 +946,17 @@ function findRouteById(id: string, treeArray: any[]) { return null; } -function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { +export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { if (!routes) return; - for (const route of routes) { + for (const route of routes.filter((item) => !item.hideInMenu)) { if (route.type === NocoBaseDesktopRouteType.page) { return route; } - if (route.children?.length) { - return findFirstPageRoute(route.children); + if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) { + const result = findFirstPageRoute(route.children); + if (result) return result; } } } diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index 53c0f11b22..38b6802b4d 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -49,6 +49,7 @@ import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { linkageAction, setInitialActionState } from './utils'; import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; +import { BlockContext } from '../../../block-provider/BlockProvider'; // 这个要放到最下面,否则会导致前端单测失败 import { useApp } from '../../../application'; @@ -96,7 +97,9 @@ export const Action: ComposedAction = withDynamicSchemaProps( const { designable } = useDesignable(); const tarComponent = useComponent(component) || component; const variables = useVariables(); - const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any }); + const localVariables = useLocalVariables({ + currentForm: { values: recordData, readPretty: false } as any, + }); const { visibleWithURL, setVisibleWithURL } = usePopupUtils(); const { setSubmitted } = useActionContext(); const { getAriaLabel } = useGetAriaLabelOfAction(title); @@ -120,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( condition: v.condition, variables, localVariables, + conditionType: v.conditionType, }, app.jsonLogic, ); @@ -155,36 +159,38 @@ export const Action: ComposedAction = withDynamicSchemaProps( }, [onClick, fieldSchema, getAllDataBlocks]); return ( - + + + ); }), { displayName: 'Action' }, diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts index 9ce1c8416c..5012fc8d64 100644 --- a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts +++ b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts @@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => { let { name: blockName } = useBlockContext() || {}; const actionTitle = title || compile(fieldSchema.title); collectionName = collectionName ? `-${collectionName}` : ''; - blockName = blockName ? `-${blockName}` : ''; + blockName = blockName && blockName !== 'action' ? `-${blockName}` : ''; action = action ? `-${action}` : ''; recordName = recordName ? `-${recordName}` : ''; diff --git a/packages/core/client/src/schema-component/antd/action/utils.ts b/packages/core/client/src/schema-component/antd/action/utils.ts index 5254021916..216035160a 100644 --- a/packages/core/client/src/schema-component/antd/action/utils.ts +++ b/packages/core/client/src/schema-component/antd/action/utils.ts @@ -87,12 +87,14 @@ export const linkageAction = async ( condition, variables, localVariables, + conditionType, }: { operator; field; condition; variables: VariablesContextType; localVariables: VariableOption[]; + conditionType: 'advanced' | 'basic'; }, jsonLogic: any, ) => { @@ -101,7 +103,7 @@ export const linkageAction = async ( switch (operator) { case ActionType.Visible: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { displayResult.push(operator); field.data = field.data || {}; field.data.hidden = false; @@ -113,7 +115,7 @@ export const linkageAction = async ( field.display = last(displayResult); break; case ActionType.Hidden: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { field.data = field.data || {}; field.data.hidden = true; } else { @@ -122,7 +124,7 @@ export const linkageAction = async ( } break; case ActionType.Disabled: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { disableResult.push(true); } field.stateOfLinkageRules = { @@ -133,7 +135,7 @@ export const linkageAction = async ( field.componentProps['disabled'] = last(disableResult); break; case ActionType.Active: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { disableResult.push(false); } else { disableResult.push(!!field.componentProps?.['disabled']); diff --git a/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts b/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts index ef95bf3b87..b01d3deb8d 100644 --- a/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts +++ b/packages/core/client/src/schema-component/antd/block-item/hooks/useGetAriaLabelOfBlockItem.ts @@ -27,7 +27,7 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => { let { name: blockName } = useBlockContext() || {}; // eslint-disable-next-line prefer-const let { name: collectionName, getField } = useCollection_deprecated(); - blockName = name || blockName; + blockName = name || (blockName !== 'action' ? blockName : ''); const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title); diff --git a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx index 5f711873f2..9e711ff07d 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx @@ -50,7 +50,7 @@ describe('CollectionSelect', () => { >
{ >
) : ( - t(field.description, { ns: NAMESPACE_UI_SCHEMA }) + field.description ); } }, [field.description]); @@ -114,9 +112,6 @@ export const FormItem: any = withDynamicSchemaProps( ? '100% !important' : null}; } - .ant-formily-item-control { - padding: ${showTitle === false ? '5px' : '0px'}; - } `, )} > diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index 62f024ccf1..715160493f 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -20,6 +20,7 @@ import { useAttach, useComponent } from '../..'; import { useApp } from '../../../application'; import { getCardItemSchema } from '../../../block-provider'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; +import { useDataBlockProps } from '../../../data-source'; import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; @@ -150,12 +151,15 @@ const WithForm = (props: WithFormProps) => { const linkageRules: any[] = (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; + // 关闭弹窗之前,如果有未保存的数据,是否要二次确认 + const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any); + useEffect(() => { const id = uid(); form.addEffects(id, () => { onFormInputChange(() => { - setFormValueChanged?.(true); + setFormValueChanged?.(confirmBeforeClose); }); }); @@ -166,7 +170,7 @@ const WithForm = (props: WithFormProps) => { return () => { form.removeEffects(id); }; - }, [form, props.disabled, setFormValueChanged]); + }, [form, props.disabled, setFormValueChanged, confirmBeforeClose]); useEffect(() => { if (loading) { @@ -219,17 +223,19 @@ const WithForm = (props: WithFormProps) => { const WithoutForm = (props) => { const fieldSchema = useFieldSchema(); const { setFormValueChanged } = useActionContext(); + // 关闭弹窗之前,如果有未保存的数据,是否要二次确认 + const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any); const form = useMemo( () => createForm({ disabled: props.disabled, effects() { onFormInputChange((form) => { - setFormValueChanged?.(true); + setFormValueChanged?.(confirmBeforeClose); }); }, }), - [], + [confirmBeforeClose], ); return fieldSchema['x-decorator'] === 'FormV2' ? ( diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index b4c932adb5..33c8194f1c 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -66,4 +66,5 @@ export * from './unix-timestamp'; export * from './upload'; export * from './variable'; export * from './form-drawer'; +export * from './linkageFilter'; import './index.less'; diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/linkageFilter/DynamicComponent.tsx new file mode 100644 index 0000000000..995043a1c9 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/linkageFilter/DynamicComponent.tsx @@ -0,0 +1,127 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { createForm, onFieldValueChange } from '@formily/core'; +import { FieldContext, FormContext } from '@formily/react'; +import { merge } from '@formily/shared'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; +import { SchemaComponent } from '../../core'; +import { FilterContext } from './context'; +import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput'; +import { useCollectionRecordData } from '../../../data-source'; +import { useLocalVariables, useVariables } from '../../../variables'; +import { useCollectionManager_deprecated } from '../../../collection-manager'; + +export interface DynamicComponentProps { + value: any; + /** + * `Filter` 组件左侧选择的字段 + */ + collectionField: CollectionFieldOptions_deprecated; + onChange: (value: any) => void; + renderSchemaComponent: () => React.JSX.Element; +} + +interface Props { + value: any; + collectionField?: CollectionFieldOptions_deprecated; + onChange: (value: any) => void; + style?: React.CSSProperties; + componentProps?: any; + schema?: any; + setScopes?: any; + testid?: string; + nullable?: boolean; + constantAbel?: boolean; + changeOnSelect?: boolean; + readOnly?: boolean; +} + +export const DynamicComponent = (props: Props) => { + const { setScopes, nullable, constantAbel, changeOnSelect, readOnly = false } = props; + const { disabled } = useContext(FilterContext) || {}; + const record = useCollectionRecordData(); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const { collectionField } = props; + const component = useCallback((props: DynamicComponentProps) => { + return ( + + ); + }, []); + const form = useMemo(() => { + return createForm({ + values: { + value: props.value, + }, + effects() { + onFieldValueChange('value', (field) => { + props?.onChange?.(field.value); + }); + }, + disabled, + }); + }, [JSON.stringify(props.value), props.schema]); + const renderSchemaComponent: any = useCallback(() => { + const componentProps = merge(props?.schema?.['x-component-props'] || {}, props.componentProps || {}); + + return ( + + + + ); + }, [props.schema]); + return ( + +
+ {React.createElement(component, { + value: props.value, + collectionField: props.collectionField, + onChange: props?.onChange, + renderSchemaComponent, + })} +
+
+ ); +}; + +export const FilterDynamicComponent = DynamicComponent; diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/FilterGroup.tsx b/packages/core/client/src/schema-component/antd/linkageFilter/FilterGroup.tsx new file mode 100644 index 0000000000..4c55b949a2 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/linkageFilter/FilterGroup.tsx @@ -0,0 +1,124 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CloseCircleOutlined } from '@ant-design/icons'; +import { ObjectField as ObjectFieldModel } from '@formily/core'; +import { ArrayField, connect, useField } from '@formily/react'; +import { Select, Space } from 'antd'; +import React, { useContext } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useToken } from '../__builtins__'; +import { FilterItems } from './FilterItems'; +import { FilterLogicContext, RemoveConditionContext } from './context'; + +export const FilterGroup = connect((props) => { + const { bordered = true, disabled } = props; + const field = useField(); + const remove = useContext(RemoveConditionContext); + const { t } = useTranslation(); + const { token } = useToken(); + + const keys = Object.keys(field.value || {}); + const logic = keys.includes('$or') ? '$or' : '$and'; + const setLogic = (value) => { + const obj = field.value || {}; + field.value = { + [value]: [...(obj[logic] || [])], + }; + }; + const mergedDisabled = disabled || field.disabled; + return ( + + + + ); +}); diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/FilterItems.tsx b/packages/core/client/src/schema-component/antd/linkageFilter/FilterItems.tsx new file mode 100644 index 0000000000..5878eaa22a --- /dev/null +++ b/packages/core/client/src/schema-component/antd/linkageFilter/FilterItems.tsx @@ -0,0 +1,33 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { ArrayField as ArrayFieldModel } from '@formily/core'; +import { ObjectField, observer, useField } from '@formily/react'; +import React from 'react'; +import { FilterGroup } from './FilterGroup'; +import { LinkageFilterItem } from './LinkageFilterItem'; +import { RemoveConditionContext } from './context'; + +export const FilterItems = observer( + (props) => { + const field = useField(); + return ( +
+ {field?.value?.filter(Boolean).map((item, index) => { + return ( + field.remove(index)}> + + + ); + })} +
+ ); + }, + { displayName: 'FilterItems' }, +); diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilter.tsx b/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilter.tsx new file mode 100644 index 0000000000..eb3003c584 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilter.tsx @@ -0,0 +1,65 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { ObjectField as ObjectFieldModel } from '@formily/core'; +import { observer, useField, useFieldSchema } from '@formily/react'; +import React, { useEffect, useState } from 'react'; +import { UseRequestOptions, useRequest } from '../../../api-client'; +import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; +import { useProps } from '../../hooks/useProps'; +import { FilterGroup } from './FilterGroup'; +import { FilterContext } from './context'; + +const useDef = (options: UseRequestOptions) => { + const field = useField(); + return useRequest(() => Promise.resolve({ data: field.dataSource }), options); +}; + +export const LinkageFilter: any = withDynamicSchemaProps( + observer((props: any) => { + const { useDataSource = useDef } = props; + + // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + const { dynamicComponent, className, collectionName } = useProps(props); + const [scopes, setScopes] = useState([]); + + const field = useField(); + const fieldSchema: any = useFieldSchema(); + useDataSource({ + onSuccess(data) { + field.dataSource = data?.data || []; + }, + }); + + useEffect(() => { + if (fieldSchema.defaultValue) { + field.initialValue = fieldSchema.defaultValue; + } + }, [fieldSchema.defaultValue]); + + return ( +
+ + + +
+ ); + }), + { displayName: 'LinkageFilter' }, +); diff --git a/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilterItem.tsx b/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilterItem.tsx new file mode 100644 index 0000000000..9f045db7cd --- /dev/null +++ b/packages/core/client/src/schema-component/antd/linkageFilter/LinkageFilterItem.tsx @@ -0,0 +1,78 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { CloseCircleOutlined } from '@ant-design/icons'; +import { css } from '@emotion/css'; +import { observer } from '@formily/react'; +import { Select, Space } from 'antd'; +import React, { useCallback, useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCompile } from '../../hooks'; +import { DynamicComponent } from './DynamicComponent'; +import { RemoveConditionContext } from './context'; +import { useValues } from './useValues'; +import { FilterContext } from './context'; + +export const LinkageFilterItem = observer( + (props: any) => { + const { t } = useTranslation(); + const compile = useCompile(); + const remove = useContext(RemoveConditionContext); + const { setScopes } = useContext(FilterContext) || {}; + const { schema, operators, operator, setOperator, rightVar, leftVar, setLeftValue, setRightValue } = useValues(); + const style = useMemo(() => ({ marginBottom: 8 }), []); + + const onOperatorsChange = useCallback( + (value) => { + setOperator(value); + }, + [setOperator], + ); + const removeStyle = useMemo(() => ({ color: '#bfbfbf' }), []); + return ( + // 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素 +
+ + + { + if (!value) { + field.setValue([]); + return; + } + field.setValue( + value.map(({ label, value }: { label: string; value: string }) => ({ + id: value, + nickname: label, + })), + ); + }} + mode="multiple" + value={value} + labelInValue={true} + onDropdownVisibleChange={(open) => setVisible(open)} + /> + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx new file mode 100644 index 0000000000..e1e3a768d2 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTable.tsx @@ -0,0 +1,263 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { + CollectionContext, + CollectionProvider_deprecated, + ResourceActionContext, + SchemaComponent, + mergeFilter, + removeNullCondition, + useFilterFieldOptions, + useFilterFieldProps, + useResourceActionContext, +} from '@nocobase/client'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useDepartmentManager } from '../hooks'; +import { Table, TablePaginationConfig, TableProps } from 'antd'; +import { departmentCollection } from '../collections/departments'; +import { useDepartmentTranslation } from '../locale'; +import { useField } from '@formily/react'; +import { Field } from '@formily/core'; +import { uid } from '@formily/shared'; +import { getDepartmentTitle } from '../utils'; + +const ExpandMetaContext = createContext({}); + +export const useFilterActionProps = () => { + const { setHasFilter, setExpandedKeys } = useContext(ExpandMetaContext); + const { t } = useDepartmentTranslation(); + const collection = useContext(CollectionContext); + const options = useFilterFieldOptions(collection.fields); + const service = useResourceActionContext(); + const { run, defaultRequest } = service; + const field = useField(); + const { params } = defaultRequest || {}; + + return { + options, + onSubmit: async (values: any) => { + // filter parameter for the block + const defaultFilter = params.filter; + // filter parameter for the filter action + const filter = removeNullCondition(values?.filter); + run({ + ...params, + page: 1, + pageSize: 10, + filter: mergeFilter([filter, defaultFilter]), + }); + const items = filter?.$and || filter?.$or; + if (items?.length) { + field.title = t('{{count}} filter items', { count: items?.length || 0 }); + setHasFilter(true); + } else { + field.title = t('Filter'); + setHasFilter(false); + } + }, + onReset() { + run({ + ...(params || {}), + filter: { + ...(params?.filter || {}), + parentId: null, + }, + page: 1, + pageSize: 10, + }); + field.title = t('Filter'); + setHasFilter(false); + setExpandedKeys([]); + }, + }; +}; + +const useDefaultDisabled = () => { + return { + disabled: () => false, + }; +}; + +const InternalDepartmentTable: React.FC<{ + useDisabled?: () => { + disabled: (record: any) => boolean; + }; +}> = ({ useDisabled = useDefaultDisabled }) => { + const { t } = useDepartmentTranslation(); + const ctx = useResourceActionContext(); + console.log(ctx); + const { run, data, loading, defaultRequest } = ctx; + const { resource, resourceOf, params } = defaultRequest || {}; + const { treeData, initData, loadData } = useDepartmentManager({ + resource, + resourceOf, + params, + }); + const field = useField(); + const { disabled } = useDisabled(); + const { hasFilter, expandedKeys, setExpandedKeys } = useContext(ExpandMetaContext); + + useEffect(() => { + if (hasFilter) { + return; + } + initData(data?.data); + }, [data, initData, loading, hasFilter]); + + const pagination: TablePaginationConfig = {}; + if (params?.pageSize) { + pagination.defaultPageSize = params.pageSize; + } + if (!pagination.total && data?.meta) { + const { count, page, pageSize } = data.meta; + pagination.total = count; + pagination.current = page; + pagination.pageSize = pageSize; + } + + return ( + (hasFilter ? getDepartmentTitle(record) : text), + }, + ] as TableProps['columns'] + } + rowSelection={{ + selectedRowKeys: (field?.value || []).map((dept: any) => dept.id), + onChange: (keys, depts) => field?.setValue?.(depts), + getCheckboxProps: (record: any) => ({ + disabled: disabled(record), + }), + }} + pagination={{ + showSizeChanger: true, + ...pagination, + onChange(page, pageSize) { + run({ + ...(ctx?.params?.[0] || {}), + page, + pageSize, + }); + }, + }} + dataSource={hasFilter ? data?.data || [] : treeData} + expandable={{ + onExpand: (expanded, record) => { + loadData({ + key: record.id, + children: record.children, + }); + }, + expandedRowKeys: expandedKeys, + onExpandedRowsChange: (keys) => setExpandedKeys(keys), + }} + /> + ); +}; + +const RequestProvider: React.FC<{ + useDataSource: any; +}> = (props) => { + const [expandedKeys, setExpandedKeys] = useState([]); + const [hasFilter, setHasFilter] = useState(false); + const { useDataSource } = props; + const service = useDataSource({ + manual: true, + }); + useEffect(() => { + service.run({ + filter: { + parentId: null, + }, + pageSize: 10, + }); + }, []); + return ( + + + + {props.children} + + + + ); +}; + +export const DepartmentTable: React.FC<{ + useDataSource: any; + useDisabled?: (record: any) => boolean; +}> = ({ useDataSource, useDisabled }) => { + return ( + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx new file mode 100644 index 0000000000..03ac4080b6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTree.tsx @@ -0,0 +1,181 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useContext, useEffect } from 'react'; +import { Tree, Dropdown, App, Empty } from 'antd'; +import { MoreOutlined } from '@ant-design/icons'; +import { useAPIClient, useResourceActionContext } from '@nocobase/client'; +import { useDepartmentTranslation } from '../locale'; +import { editDepartmentSchema, newSubDepartmentSchema } from './schemas/departments'; +import { ResourcesContext } from '../ResourcesProvider'; +import { DepartmentTreeContext } from './Department'; +import { css } from '@emotion/css'; + +type DepartmentTreeProps = { + node: { + id: number; + title: string; + parent?: any; + }; + setVisible: (visible: boolean) => void; + setDrawer: (schema: any) => void; +}; + +export const DepartmentTree: React.FC & { + Item: React.FC; +} = () => { + const { data, loading } = useResourceActionContext(); + const { department, setDepartment, setUser } = useContext(ResourcesContext); + const { treeData, nodeMap, loadData, loadedKeys, setLoadedKeys, initData, expandedKeys, setExpandedKeys } = + useContext(DepartmentTreeContext); + const handleSelect = (keys: number[]) => { + if (!keys.length) { + return; + } + const node = nodeMap[keys[0]]; + setDepartment(node); + setUser(null); + }; + + const handleExpand = (keys: number[]) => { + setExpandedKeys(keys); + }; + + const handleLoad = (keys: number[]) => { + setLoadedKeys(keys); + }; + + useEffect(() => { + initData(data?.data); + }, [data, initData, loading]); + + useEffect(() => { + if (!department) { + return; + } + const getIds = (node: any) => { + if (node.parent) { + return [node.parent.id, ...getIds(node.parent)]; + } + return []; + }; + const newKeys = getIds(department); + setExpandedKeys((keys) => Array.from(new Set([...keys, ...newKeys]))); + }, [department, setExpandedKeys]); + + return ( +
+ {treeData?.length ? ( + + ) : ( + + )} +
+ ); +}; + +DepartmentTree.Item = function DepartmentTreeItem({ node, setVisible, setDrawer }: DepartmentTreeProps) { + const { t } = useDepartmentTranslation(); + const { refreshAsync } = useResourceActionContext(); + const { setLoadedKeys, expandedKeys, setExpandedKeys } = useContext(DepartmentTreeContext); + const { modal, message } = App.useApp(); + const api = useAPIClient(); + const deleteDepartment = () => { + modal.confirm({ + title: t('Delete'), + content: t('Are you sure you want to delete it?'), + onOk: async () => { + await api.resource('departments').destroy({ filterByTk: node.id }); + message.success(t('Deleted successfully')); + setExpandedKeys((keys) => keys.filter((k) => k !== node.id)); + const expanded = [...expandedKeys]; + setLoadedKeys([]); + setExpandedKeys([]); + await refreshAsync(); + setExpandedKeys(expanded); + }, + }); + }; + const openDrawer = (schema: any) => { + setDrawer({ schema, node }); + setVisible(true); + }; + const handleClick = ({ key, domEvent }) => { + domEvent.stopPropagation(); + switch (key) { + case 'new-sub': + openDrawer(newSubDepartmentSchema); + break; + case 'edit': + openDrawer(editDepartmentSchema); + break; + case 'delete': + deleteDepartment(); + } + }; + return ( +
+
{node.title}
+ +
+ +
+
+
+ ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx new file mode 100644 index 0000000000..b66b511548 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/DepartmentTreeSelect.tsx @@ -0,0 +1,136 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useCallback, useContext, useEffect } from 'react'; +import { TreeSelect } from 'antd'; +import { useField } from '@formily/react'; +import { Field } from '@formily/core'; +import { useRecord } from '@nocobase/client'; +import { ResourcesContext } from '../ResourcesProvider'; +import { useDepartmentManager } from '../hooks/departments-manager'; + +export const DepartmentTreeSelect: React.FC<{ + originData: any; + treeData: any[]; + [key: string]: any; +}> = (props) => { + const field = useField(); + const [value, setValue] = React.useState({ label: null, value: null }); + const { treeData, initData, getByKeyword, loadData, loadedKeys, setLoadedKeys, originData } = props; + + const handleSearch = async (keyword: string) => { + if (!keyword) { + initData(originData); + return; + } + await getByKeyword(keyword); + }; + + const getTitle = useCallback((record: any) => { + const title = record.title; + const parent = record.parent; + if (parent) { + return getTitle(parent) + ' / ' + title; + } + return title; + }, []); + + useEffect(() => { + initData(originData); + }, [originData, initData]); + + useEffect(() => { + if (!field.value) { + setValue({ label: null, value: null }); + return; + } + setValue({ + label: getTitle(field.value) || field.value.label, + value: field.value.id, + }); + }, [field.value, getTitle]); + + return ( + { + field.setValue(node); + }} + onChange={(value: any) => { + if (!value) { + field.setValue(null); + } + }} + treeData={treeData} + treeLoadedKeys={loadedKeys} + onTreeLoad={(keys: any[]) => setLoadedKeys(keys)} + loadData={(node: any) => loadData({ key: node.id, children: node.children })} + fieldNames={{ + value: 'id', + }} + showSearch + allowClear + treeNodeFilterProp="title" + onSearch={handleSearch} + labelInValue={true} + /> + ); +}; + +export const DepartmentSelect: React.FC = () => { + const departmentManager = useDepartmentManager(); + const { departmentsResource } = useContext(ResourcesContext); + const { + service: { data }, + } = departmentsResource || {}; + return ; +}; + +export const SuperiorDepartmentSelect: React.FC = () => { + const departmentManager = useDepartmentManager(); + const { setTreeData, getChildrenIds } = departmentManager; + const record = useRecord() as any; + const { departmentsResource } = useContext(ResourcesContext); + const { + service: { data }, + } = departmentsResource || {}; + + useEffect(() => { + if (!record.id) { + return; + } + const childrenIds = getChildrenIds(record.id); + childrenIds.push(record.id); + setTreeData((treeData) => { + const setDisabled = (treeData: any[]) => { + return treeData.map((node) => { + if (childrenIds.includes(node.id)) { + node.disabled = true; + } + if (node.children) { + node.children = setDisabled(node.children); + } + return node; + }); + }; + return setDisabled(treeData); + }); + }, [setTreeData, record.id, getChildrenIds]); + + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx new file mode 100644 index 0000000000..e685d2d89a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/IsOwnerField.tsx @@ -0,0 +1,30 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useContext } from 'react'; +import { useDepartmentTranslation } from '../locale'; +import { Checkbox, useRecord } from '@nocobase/client'; +import { ResourcesContext } from '../ResourcesProvider'; + +export const IsOwnerField: React.FC = () => { + const { department } = useContext(ResourcesContext); + const record = useRecord() as any; + const dept = (record.departments || []).find((dept: any) => dept?.id === department?.id); + + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx new file mode 100644 index 0000000000..a23aa2bde8 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/Member.tsx @@ -0,0 +1,235 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import React, { useContext, useRef, useEffect, useMemo } from 'react'; +import { useDepartmentTranslation } from '../locale'; +import { + CollectionContext, + ResourceActionProvider, + SchemaComponent, + useAPIClient, + useActionContext, + useFilterFieldOptions, + useFilterFieldProps, + useRecord, + useResourceActionContext, + useTableBlockContext, +} from '@nocobase/client'; +import { membersActionSchema, addMembersSchema, rowRemoveActionSchema, getMembersSchema } from './schemas/users'; +import { App } from 'antd'; +import { DepartmentField } from './DepartmentField'; +import { IsOwnerField } from './IsOwnerField'; +import { UserDepartmentsField } from './UserDepartmentsField'; +import { ResourcesContext } from '../ResourcesProvider'; +import { useTableBlockProps } from '../hooks/useTableBlockProps'; + +const AddMembersListProvider: React.FC = (props) => { + const { department } = useContext(ResourcesContext); + return ( + + {props.children} + + ); +}; + +const useAddMembersFilterActionProps = () => { + const collection = useContext(CollectionContext); + const options = useFilterFieldOptions(collection.fields); + const service = useResourceActionContext(); + return useFilterFieldProps({ + options, + params: service.state?.params?.[0] || service.params, + service, + }); +}; + +export const AddMembers: React.FC = () => { + const { department } = useContext(ResourcesContext); + // This resource is the list of members of the current department. + const { + service: { refresh }, + } = useTableBlockContext(); + const selectedKeys = useRef([]); + const api = useAPIClient(); + + const useAddMembersActionProps = () => { + const { department } = useContext(ResourcesContext); + const { setVisible } = useActionContext(); + return { + async onClick() { + const selected = selectedKeys.current; + if (!selected?.length) { + return; + } + await api.resource('departments.members', department.id).add({ + values: selected, + }); + selectedKeys.current = []; + refresh(); + setVisible?.(false); + }, + }; + }; + + const handleSelect = (keys: any[]) => { + selectedKeys.current = keys; + }; + + return ( + + ); +}; + +const useBulkRemoveMembersAction = () => { + const { t } = useDepartmentTranslation(); + const { message } = App.useApp(); + const api = useAPIClient(); + const { + service: { refresh }, + field, + } = useTableBlockContext(); + const { department } = useContext(ResourcesContext); + return { + async run() { + const selected = field?.data?.selectedRowKeys; + if (!selected?.length) { + message.warning(t('Please select members')); + return; + } + await api.resource('departments.members', department.id).remove({ + values: selected, + }); + field.data.selectedRowKeys = []; + refresh(); + }, + }; +}; + +const useRemoveMemberAction = () => { + const api = useAPIClient(); + const { department } = useContext(ResourcesContext); + const { id } = useRecord() as any; + const { + service: { refresh }, + } = useTableBlockContext(); + return { + async run() { + await api.resource('departments.members', department.id).remove({ + values: [id], + }); + refresh(); + }, + }; +}; + +const useShowTotal = () => { + const { + service: { data }, + } = useTableBlockContext(); + const { t } = useDepartmentTranslation(); + return t('Total {{count}} members', { count: data?.meta?.count }); +}; + +const useRefreshActionProps = () => { + const { service } = useTableBlockContext(); + return { + async onClick() { + service?.refresh?.(); + }, + }; +}; + +const RowRemoveAction = () => { + const { department } = useContext(ResourcesContext); + return department ? : null; +}; + +const MemberActions = () => { + const { department } = useContext(ResourcesContext); + return department ? : null; +}; + +const useMemberFilterActionProps = () => { + const collection = useContext(CollectionContext); + const options = useFilterFieldOptions(collection.fields); + const { service } = useTableBlockContext(); + return useFilterFieldProps({ + options, + params: service.state?.params?.[0] || service.params, + service, + }); +}; + +export const Member: React.FC = () => { + const { t } = useDepartmentTranslation(); + const { department, user } = useContext(ResourcesContext); + const { + service: { data, setState }, + } = useTableBlockContext(); + + useEffect(() => { + setState?.({ selectedRowKeys: [] }); + }, [data, setState]); + + const schema = useMemo(() => getMembersSchema(department, user), [department, user]); + + return ( + <> + {!user ?

{t(department?.title || 'All users')}

:

{t('Search results')}

} + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx new file mode 100644 index 0000000000..08f4e78a9f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/NewDepartment.tsx @@ -0,0 +1,97 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { SchemaComponent } from '@nocobase/client'; +import React from 'react'; +import { useDepartmentTranslation } from '../locale'; + +export const NewDepartment: React.FC = () => { + const { t } = useDepartmentTranslation(); + return ( + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx b/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx new file mode 100644 index 0000000000..e72869046a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-departments/src/client/departments/UserDepartmentsField.tsx @@ -0,0 +1,273 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This program is offered under a commercial license. + * For more information, see + */ + +import { + ActionContextProvider, + SchemaComponent, + useAPIClient, + useRecord, + useRequest, + useResourceActionContext, + useTableBlockContext, +} from '@nocobase/client'; +import React, { useState } from 'react'; +import { Tag, Button, Dropdown, App } from 'antd'; +import { PlusOutlined, MoreOutlined } from '@ant-design/icons'; +import { Field } from '@formily/core'; +import { useField, useForm } from '@formily/react'; +import { userDepartmentsSchema } from './schemas/users'; +import { getDepartmentTitle } from '../utils'; +import { useDepartmentTranslation } from '../locale'; +import { DepartmentTable } from './DepartmentTable'; + +const useDataSource = (options?: any) => { + const defaultRequest = { + resource: 'departments', + action: 'list', + params: { + appends: ['parent(recursively=true)'], + // filter: { + // parentId: null, + // }, + sort: ['createdAt'], + }, + }; + const service = useRequest(defaultRequest, options); + return { + ...service, + defaultRequest, + }; +}; + +export const UserDepartmentsField: React.FC = () => { + const { modal, message } = App.useApp(); + const { t } = useDepartmentTranslation(); + const [visible, setVisible] = useState(false); + const user = useRecord() as any; + const field = useField(); + const { + service: { refresh }, + } = useTableBlockContext(); + + const formatData = (data: any[]) => { + if (!data?.length) { + return []; + } + + return data.map((department) => ({ + ...department, + isMain: department.departmentsUsers?.isMain, + isOwner: department.departmentsUsers?.isOwner, + title: getDepartmentTitle(department), + })); + }; + + const api = useAPIClient(); + useRequest( + () => + api + .resource(`users.departments`, user.id) + .list({ + appends: ['parent(recursively=true)'], + paginate: false, + }) + .then((res) => { + const data = formatData(res?.data?.data); + field.setValue(data); + }), + { + ready: user.id, + }, + ); + + const useAddDepartments = () => { + const api = useAPIClient(); + const drawerForm = useForm(); + const { departments } = drawerForm.values || {}; + return { + async run() { + await api.resource('users.departments', user.id).add({ + values: departments.map((dept: any) => dept.id), + }); + drawerForm.reset(); + field.setValue([ + ...field.value, + ...departments.map((dept: any, index: number) => ({ + ...dept, + isMain: index === 0 && field.value.length === 0, + title: getDepartmentTitle(dept), + })), + ]); + setVisible(false); + refresh(); + }, + }; + }; + + const removeDepartment = (dept: any) => { + modal.confirm({ + title: t('Remove department'), + content: t('Are you sure you want to remove it?'), + onOk: async () => { + await api.resource('users.departments', user.id).remove({ values: [dept.id] }); + message.success(t('Deleted successfully')); + field.setValue( + field.value + .filter((d: any) => d.id !== dept.id) + .map((d: any, index: number) => ({ + ...d, + isMain: (dept.isMain && index === 0) || d.isMain, + })), + ); + refresh(); + }, + }); + }; + + const setMainDepartment = async (dept: any) => { + await api.resource('users').setMainDepartment({ + values: { + userId: user.id, + departmentId: dept.id, + }, + }); + message.success(t('Set successfully')); + field.setValue( + field.value.map((d: any) => ({ + ...d, + isMain: d.id === dept.id, + })), + ); + refresh(); + }; + + const setOwner = async (dept: any) => { + await api.resource('departments').setOwner({ + values: { + userId: user.id, + departmentId: dept.id, + }, + }); + message.success(t('Set successfully')); + field.setValue( + field.value.map((d: any) => ({ + ...d, + isOwner: d.id === dept.id ? true : d.isOwner, + })), + ); + refresh(); + }; + + const removeOwner = async (dept: any) => { + await api.resource('departments').removeOwner({ + values: { + userId: user.id, + departmentId: dept.id, + }, + }); + message.success(t('Set successfully')); + field.setValue( + field.value.map((d: any) => ({ + ...d, + isOwner: d.id === dept.id ? false : d.isOwner, + })), + ); + refresh(); + }; + + const handleClick = (key: string, dept: any) => { + switch (key) { + case 'setMain': + setMainDepartment(dept); + break; + case 'setOwner': + setOwner(dept); + break; + case 'removeOwner': + removeOwner(dept); + break; + case 'remove': + removeDepartment(dept); + } + }; + + const useDisabled = () => ({ + disabled: (record: any) => { + return field.value.some((dept: any) => dept.id === record.id); + }, + }); + + return ( + + <> + {(field?.value || []).map((dept) => ( + + {dept.title} + {dept.isMain ? ( + + {t('Main')} + + ) : ( + '' + )} + {/* {dept.isOwner ? ( */} + {/* */} + {/* {t('Owner')} */} + {/* */} + {/* ) : ( */} + {/* '' */} + {/* )} */} + handleClick(key, dept), + }} + > +
+ +
+
+
+ ))} +