From 29433aa441b90481f6a36992a5201a904507edef Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 26 Aug 2024 00:16:25 +0800 Subject: [PATCH 01/68] feat: 1.4.0-alpha --- lerna.json | 6 +- packages/core/acl/package.json | 6 +- packages/core/actions/package.json | 8 +- packages/core/app/package.json | 10 +- packages/core/auth/package.json | 12 +- packages/core/build/package.json | 2 +- packages/core/cache/package.json | 2 +- packages/core/cli/package.json | 6 +- packages/core/client/package.json | 8 +- .../core/create-nocobase-app/package.json | 2 +- .../core/data-source-manager/package.json | 12 +- packages/core/database/package.json | 6 +- packages/core/devtools/package.json | 8 +- packages/core/evaluators/package.json | 4 +- packages/core/logger/package.json | 2 +- packages/core/resourcer/package.json | 4 +- packages/core/sdk/package.json | 2 +- packages/core/server/package.json | 26 ++-- packages/core/telemetry/package.json | 4 +- packages/core/test/package.json | 4 +- packages/core/utils/package.json | 2 +- .../plugins/@nocobase/plugin-acl/package.json | 2 +- .../plugin-action-bulk-edit/package.json | 2 +- .../plugin-action-bulk-update/package.json | 2 +- .../plugin-action-custom-request/package.json | 2 +- .../plugin-action-duplicate/package.json | 2 +- .../plugin-action-export/package.json | 2 +- .../plugin-action-import/package.json | 2 +- .../plugin-action-print/package.json | 2 +- .../@nocobase/plugin-api-doc/package.json | 2 +- .../@nocobase/plugin-api-keys/package.json | 2 +- .../@nocobase/plugin-audit-logs/package.json | 2 +- .../@nocobase/plugin-auth-sms/package.json | 2 +- .../@nocobase/plugin-auth/package.json | 2 +- .../plugin-backup-restore/package.json | 2 +- .../plugin-block-iframe/package.json | 2 +- .../plugin-block-workbench/package.json | 2 +- .../@nocobase/plugin-calendar/package.json | 2 +- .../@nocobase/plugin-charts/package.json | 2 +- .../@nocobase/plugin-client/package.json | 2 +- .../plugin-collection-sql/package.json | 2 +- .../plugin-collection-tree/package.json | 2 +- .../plugin-data-source-main/package.json | 2 +- .../plugin-data-source-manager/package.json | 2 +- .../plugin-data-visualization/package.json | 2 +- .../plugin-disable-pm-add/package.json | 2 +- .../plugin-error-handler/package.json | 2 +- .../plugin-field-china-region/package.json | 2 +- .../plugin-field-formula/package.json | 2 +- .../plugin-field-m2m-array/package.json | 2 +- .../plugin-field-markdown-vditor/package.json | 2 +- .../plugin-field-sequence/package.json | 2 +- .../plugin-file-manager/package.json | 2 +- .../@nocobase/plugin-gantt/package.json | 2 +- .../package.json | 2 +- .../@nocobase/plugin-kanban/package.json | 2 +- .../plugin-localization/package.json | 2 +- .../@nocobase/plugin-logger/package.json | 2 +- .../plugins/@nocobase/plugin-map/package.json | 2 +- .../plugin-mobile-client/package.json | 2 +- .../@nocobase/plugin-mobile/package.json | 2 +- .../plugin-mock-collections/package.json | 2 +- .../plugin-multi-app-manager/package.json | 2 +- .../package.json | 2 +- .../plugin-notifications/package.json | 2 +- .../plugin-sample-hello/package.json | 2 +- .../plugin-snapshot-field/package.json | 2 +- .../plugin-system-settings/package.json | 2 +- .../plugin-theme-editor/package.json | 2 +- .../plugin-ui-schema-storage/package.json | 2 +- .../@nocobase/plugin-users/package.json | 2 +- .../plugin-verification/package.json | 2 +- .../package.json | 2 +- .../plugin-workflow-aggregate/package.json | 2 +- .../plugin-workflow-delay/package.json | 2 +- .../package.json | 2 +- .../plugin-workflow-loop/package.json | 2 +- .../plugin-workflow-mailer/package.json | 2 +- .../plugin-workflow-manual/package.json | 2 +- .../plugin-workflow-parallel/package.json | 2 +- .../plugin-workflow-request/package.json | 2 +- .../plugin-workflow-sql/package.json | 2 +- .../plugin-workflow-test/package.json | 2 +- .../@nocobase/plugin-workflow/package.json | 4 +- packages/presets/nocobase/package.json | 122 +++++++++--------- 85 files changed, 192 insertions(+), 194 deletions(-) diff --git a/lerna.json b/lerna.json index 8d8b32d303..9710afb03d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,10 +1,8 @@ { - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json index ce5d6db72e..607220f2a3 100644 --- a/packages/core/acl/package.json +++ b/packages/core/acl/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/acl", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/resourcer": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/resourcer": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "minimatch": "^5.1.1" }, "repository": { diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index a3f405be51..bd259c9bb2 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -1,14 +1,14 @@ { "name": "@nocobase/actions", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/cache": "1.3.0-beta", - "@nocobase/database": "1.3.0-beta", - "@nocobase/resourcer": "1.3.0-beta" + "@nocobase/cache": "1.4.0-alpha", + "@nocobase/database": "1.4.0-alpha", + "@nocobase/resourcer": "1.4.0-alpha" }, "repository": { "type": "git", diff --git a/packages/core/app/package.json b/packages/core/app/package.json index f6ddf9ada2..8efe8f61f2 100644 --- a/packages/core/app/package.json +++ b/packages/core/app/package.json @@ -1,17 +1,17 @@ { "name": "@nocobase/app", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/database": "1.3.0-beta", - "@nocobase/preset-nocobase": "1.3.0-beta", - "@nocobase/server": "1.3.0-beta" + "@nocobase/database": "1.4.0-alpha", + "@nocobase/preset-nocobase": "1.4.0-alpha", + "@nocobase/server": "1.4.0-alpha" }, "devDependencies": { - "@nocobase/client": "1.3.0-beta" + "@nocobase/client": "1.4.0-alpha" }, "repository": { "type": "git", diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index 8063b98093..af0e356596 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -1,16 +1,16 @@ { "name": "@nocobase/auth", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.3.0-beta", - "@nocobase/cache": "1.3.0-beta", - "@nocobase/database": "1.3.0-beta", - "@nocobase/resourcer": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/actions": "1.4.0-alpha", + "@nocobase/cache": "1.4.0-alpha", + "@nocobase/database": "1.4.0-alpha", + "@nocobase/resourcer": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" }, diff --git a/packages/core/build/package.json b/packages/core/build/package.json index 9c6a762278..4d937611b1 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/build", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "Library build tool based on rollup.", "main": "lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json index 2da0bc0c70..1350f24464 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cache", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index 83a18015af..ebc7b0eb5e 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cli", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", @@ -8,7 +8,7 @@ "nocobase": "./bin/index.js" }, "dependencies": { - "@nocobase/app": "1.3.0-beta", + "@nocobase/app": "1.4.0-alpha", "@types/fs-extra": "^11.0.1", "@umijs/utils": "3.5.20", "chalk": "^4.1.1", @@ -25,7 +25,7 @@ "tsx": "^4.6.2" }, "devDependencies": { - "@nocobase/devtools": "1.3.0-beta" + "@nocobase/devtools": "1.4.0-alpha" }, "repository": { "type": "git", diff --git a/packages/core/client/package.json b/packages/core/client/package.json index ab830a176a..d927c9e61b 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/client", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "lib/index.js", "module": "es/index.mjs", @@ -26,9 +26,9 @@ "@formily/reactive-react": "^2.2.27", "@formily/shared": "^2.2.27", "@formily/validator": "^2.2.27", - "@nocobase/evaluators": "1.3.0-beta", - "@nocobase/sdk": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/evaluators": "1.4.0-alpha", + "@nocobase/sdk": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "ahooks": "^3.7.2", "antd": "^5.12.8", "antd-style": "3.4.5", diff --git a/packages/core/create-nocobase-app/package.json b/packages/core/create-nocobase-app/package.json index 08daa82746..d038578542 100755 --- a/packages/core/create-nocobase-app/package.json +++ b/packages/core/create-nocobase-app/package.json @@ -1,6 +1,6 @@ { "name": "create-nocobase-app", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "src/index.js", "license": "AGPL-3.0", "dependencies": { diff --git a/packages/core/data-source-manager/package.json b/packages/core/data-source-manager/package.json index bf06b066ba..f1b37c4978 100644 --- a/packages/core/data-source-manager/package.json +++ b/packages/core/data-source-manager/package.json @@ -1,16 +1,16 @@ { "name": "@nocobase/data-source-manager", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.3.0-beta", - "@nocobase/cache": "1.3.0-beta", - "@nocobase/database": "1.3.0-beta", - "@nocobase/resourcer": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/actions": "1.4.0-alpha", + "@nocobase/cache": "1.4.0-alpha", + "@nocobase/database": "1.4.0-alpha", + "@nocobase/resourcer": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" }, diff --git a/packages/core/database/package.json b/packages/core/database/package.json index b2453e1e64..acd51010c6 100644 --- a/packages/core/database/package.json +++ b/packages/core/database/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/database", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "main": "./lib/index.js", "types": "./lib/index.d.ts", "license": "AGPL-3.0", "dependencies": { - "@nocobase/logger": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/logger": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "async-mutex": "^0.3.2", "chalk": "^4.1.1", "cron-parser": "4.4.0", diff --git a/packages/core/devtools/package.json b/packages/core/devtools/package.json index d2ba648143..9b2ffea102 100644 --- a/packages/core/devtools/package.json +++ b/packages/core/devtools/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/devtools", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", "dependencies": { - "@nocobase/build": "1.3.0-beta", - "@nocobase/client": "1.3.0-beta", - "@nocobase/test": "1.3.0-beta", + "@nocobase/build": "1.4.0-alpha", + "@nocobase/client": "1.4.0-alpha", + "@nocobase/test": "1.4.0-alpha", "@types/koa": "^2.13.4", "@types/koa-bodyparser": "^4.3.4", "@types/lodash": "^4.14.177", diff --git a/packages/core/evaluators/package.json b/packages/core/evaluators/package.json index ec97d62e54..fdaf6ac29f 100644 --- a/packages/core/evaluators/package.json +++ b/packages/core/evaluators/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/evaluators", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "main": "./lib/index.js", "types": "./lib/index.d.ts", "license": "AGPL-3.0", "dependencies": { "@formulajs/formulajs": "4.2.0", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/utils": "1.4.0-alpha", "mathjs": "^10.6.0" }, "repository": { diff --git a/packages/core/logger/package.json b/packages/core/logger/package.json index 44840788a3..86417b9675 100644 --- a/packages/core/logger/package.json +++ b/packages/core/logger/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/logger", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "nocobase logging library", "license": "AGPL-3.0", "main": "./lib/index.js", diff --git a/packages/core/resourcer/package.json b/packages/core/resourcer/package.json index c004e2a3f7..2ca490b9cb 100644 --- a/packages/core/resourcer/package.json +++ b/packages/core/resourcer/package.json @@ -1,12 +1,12 @@ { "name": "@nocobase/resourcer", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "main": "./lib/index.js", "types": "./lib/index.d.ts", "license": "AGPL-3.0", "dependencies": { - "@nocobase/utils": "1.3.0-beta", + "@nocobase/utils": "1.4.0-alpha", "deepmerge": "^4.2.2", "koa-compose": "^4.1.0", "lodash": "^4.17.21", diff --git a/packages/core/sdk/package.json b/packages/core/sdk/package.json index 34fe30c7de..e88a5c0f8e 100644 --- a/packages/core/sdk/package.json +++ b/packages/core/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/sdk", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/packages/core/server/package.json b/packages/core/server/package.json index 9391393204..d353adeb32 100644 --- a/packages/core/server/package.json +++ b/packages/core/server/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/server", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "lib/index.js", "types": "./lib/index.d.ts", "license": "AGPL-3.0", @@ -10,18 +10,18 @@ "@koa/cors": "^3.1.0", "@koa/multer": "^3.0.2", "@koa/router": "^9.4.0", - "@nocobase/acl": "1.3.0-beta", - "@nocobase/actions": "1.3.0-beta", - "@nocobase/auth": "1.3.0-beta", - "@nocobase/cache": "1.3.0-beta", - "@nocobase/data-source-manager": "1.3.0-beta", - "@nocobase/database": "1.3.0-beta", - "@nocobase/evaluators": "1.3.0-beta", - "@nocobase/logger": "1.3.0-beta", - "@nocobase/resourcer": "1.3.0-beta", - "@nocobase/sdk": "1.3.0-beta", - "@nocobase/telemetry": "1.3.0-beta", - "@nocobase/utils": "1.3.0-beta", + "@nocobase/acl": "1.4.0-alpha", + "@nocobase/actions": "1.4.0-alpha", + "@nocobase/auth": "1.4.0-alpha", + "@nocobase/cache": "1.4.0-alpha", + "@nocobase/data-source-manager": "1.4.0-alpha", + "@nocobase/database": "1.4.0-alpha", + "@nocobase/evaluators": "1.4.0-alpha", + "@nocobase/logger": "1.4.0-alpha", + "@nocobase/resourcer": "1.4.0-alpha", + "@nocobase/sdk": "1.4.0-alpha", + "@nocobase/telemetry": "1.4.0-alpha", + "@nocobase/utils": "1.4.0-alpha", "@types/decompress": "4.2.4", "@types/ini": "^1.3.31", "@types/koa-send": "^4.1.3", diff --git a/packages/core/telemetry/package.json b/packages/core/telemetry/package.json index 25e206e5fb..197890a26c 100644 --- a/packages/core/telemetry/package.json +++ b/packages/core/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/telemetry", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "nocobase telemetry library", "license": "AGPL-3.0", "main": "./lib/index.js", @@ -11,7 +11,7 @@ "directory": "packages/telemetry" }, "dependencies": { - "@nocobase/utils": "1.3.0-beta", + "@nocobase/utils": "1.4.0-alpha", "@opentelemetry/api": "^1.7.0", "@opentelemetry/instrumentation": "^0.46.0", "@opentelemetry/resources": "^1.19.0", diff --git a/packages/core/test/package.json b/packages/core/test/package.json index 6956382a5c..11642c491e 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/test", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "lib/index.js", "module": "./src/index.ts", "types": "./lib/index.d.ts", @@ -51,7 +51,7 @@ }, "dependencies": { "@faker-js/faker": "8.1.0", - "@nocobase/server": "1.3.0-beta", + "@nocobase/server": "1.4.0-alpha", "@playwright/test": "^1.45.3", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", diff --git a/packages/core/utils/package.json b/packages/core/utils/package.json index e173441215..83f3327ad5 100644 --- a/packages/core/utils/package.json +++ b/packages/core/utils/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/utils", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "lib/index.js", "types": "./lib/index.d.ts", "license": "AGPL-3.0", diff --git a/packages/plugins/@nocobase/plugin-acl/package.json b/packages/plugins/@nocobase/plugin-acl/package.json index 465f584ef6..3ced4426eb 100644 --- a/packages/plugins/@nocobase/plugin-acl/package.json +++ b/packages/plugins/@nocobase/plugin-acl/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "权限控制", "description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.", "description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/acl", diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json b/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json index 069aaf4365..06eea94861 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-action-bulk-edit", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-bulk-edit", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-edit", diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/package.json b/packages/plugins/@nocobase/plugin-action-bulk-update/package.json index 562f1b20d2..f511dd6f66 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-update/package.json +++ b/packages/plugins/@nocobase/plugin-action-bulk-update/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-action-bulk-update", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-bulk-update", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-bulk-update", diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/package.json b/packages/plugins/@nocobase/plugin-action-custom-request/package.json index cd7678a7a4..9467e68bce 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/package.json +++ b/packages/plugins/@nocobase/plugin-action-custom-request/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-action-custom-request", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-custom-request", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-custom-request", diff --git a/packages/plugins/@nocobase/plugin-action-duplicate/package.json b/packages/plugins/@nocobase/plugin-action-duplicate/package.json index 489754b2b9..d21fe9a365 100644 --- a/packages/plugins/@nocobase/plugin-action-duplicate/package.json +++ b/packages/plugins/@nocobase/plugin-action-duplicate/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-action-duplicate", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-duplicate", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-duplicate", diff --git a/packages/plugins/@nocobase/plugin-action-export/package.json b/packages/plugins/@nocobase/plugin-action-export/package.json index e2f6a8c3f2..72d0672d28 100644 --- a/packages/plugins/@nocobase/plugin-action-export/package.json +++ b/packages/plugins/@nocobase/plugin-action-export/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "操作:导出记录", "description": "Export filtered records to excel, you can configure which fields to export.", "description.zh-CN": "导出筛选后的记录到 Excel 中,可以配置导出哪些字段。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-export", diff --git a/packages/plugins/@nocobase/plugin-action-import/package.json b/packages/plugins/@nocobase/plugin-action-import/package.json index 0af786a5ef..eb27d8035e 100644 --- a/packages/plugins/@nocobase/plugin-action-import/package.json +++ b/packages/plugins/@nocobase/plugin-action-import/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "操作:导入记录", "description": "Import records using excel templates. You can configure which fields to import and templates will be generated automatically.", "description.zh-CN": "使用 Excel 模板导入数据,可以配置导入哪些字段,自动生成模板。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-import", diff --git a/packages/plugins/@nocobase/plugin-action-print/package.json b/packages/plugins/@nocobase/plugin-action-print/package.json index adfab22555..cf6fff5038 100644 --- a/packages/plugins/@nocobase/plugin-action-print/package.json +++ b/packages/plugins/@nocobase/plugin-action-print/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-action-print", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/action-print", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/action-print", diff --git a/packages/plugins/@nocobase/plugin-api-doc/package.json b/packages/plugins/@nocobase/plugin-api-doc/package.json index 06a1f0ed2f..900fdcc674 100644 --- a/packages/plugins/@nocobase/plugin-api-doc/package.json +++ b/packages/plugins/@nocobase/plugin-api-doc/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-api-doc", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "API documentation", "displayName.zh-CN": "API 文档", "description": "An OpenAPI documentation generator for NocoBase HTTP API.", diff --git a/packages/plugins/@nocobase/plugin-api-keys/package.json b/packages/plugins/@nocobase/plugin-api-keys/package.json index e59e79c99b..9562263bab 100644 --- a/packages/plugins/@nocobase/plugin-api-keys/package.json +++ b/packages/plugins/@nocobase/plugin-api-keys/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "认证:API 密钥", "description": "Allows users to use API key to access application's HTTP API", "description.zh-CN": "允许用户使用 API 密钥访问应用的 HTTP API", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/api-keys", diff --git a/packages/plugins/@nocobase/plugin-audit-logs/package.json b/packages/plugins/@nocobase/plugin-audit-logs/package.json index b7ec07bb87..6412b18151 100644 --- a/packages/plugins/@nocobase/plugin-audit-logs/package.json +++ b/packages/plugins/@nocobase/plugin-audit-logs/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-audit-logs", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Audit logs (deprecated)", "displayName.zh-CN": "审计日志(废弃)", "description": "This plugin is deprecated. There will be a new audit log plugin in the future.", diff --git a/packages/plugins/@nocobase/plugin-auth-sms/package.json b/packages/plugins/@nocobase/plugin-auth-sms/package.json index 04cf3202b0..cd90bfead4 100644 --- a/packages/plugins/@nocobase/plugin-auth-sms/package.json +++ b/packages/plugins/@nocobase/plugin-auth-sms/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "认证:短信", "description": "SMS authentication.", "description.zh-CN": "通过短信验证码认证身份。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/auth-sms", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth-sms", diff --git a/packages/plugins/@nocobase/plugin-auth/package.json b/packages/plugins/@nocobase/plugin-auth/package.json index ac931a000d..3a35767cc7 100644 --- a/packages/plugins/@nocobase/plugin-auth/package.json +++ b/packages/plugins/@nocobase/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-auth", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/auth", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/auth", diff --git a/packages/plugins/@nocobase/plugin-backup-restore/package.json b/packages/plugins/@nocobase/plugin-backup-restore/package.json index c42222b806..835006764a 100644 --- a/packages/plugins/@nocobase/plugin-backup-restore/package.json +++ b/packages/plugins/@nocobase/plugin-backup-restore/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "应用的备份与还原", "description": "Backup and restore applications for scenarios such as application replication, migration, and upgrades.", "description.zh-CN": "备份和还原应用,可用于应用的复制、迁移、升级等场景。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/backup-restore", diff --git a/packages/plugins/@nocobase/plugin-block-iframe/package.json b/packages/plugins/@nocobase/plugin-block-iframe/package.json index b9e2a0e5dc..df9f349994 100644 --- a/packages/plugins/@nocobase/plugin-block-iframe/package.json +++ b/packages/plugins/@nocobase/plugin-block-iframe/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "区块:iframe", "description": "Create an iframe block on the page to embed and display external web pages or content.", "description.zh-CN": "在页面上创建和管理iframe,用于嵌入和展示外部网页或内容。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/block-iframe", diff --git a/packages/plugins/@nocobase/plugin-block-workbench/package.json b/packages/plugins/@nocobase/plugin-block-workbench/package.json index 05921ae1aa..83d51ecfcf 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/package.json +++ b/packages/plugins/@nocobase/plugin-block-workbench/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-block-workbench", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Block: Workbench", "displayName.zh-CN": "区块:工作台", "description": "Add buttons for actions, links, etc. in the workbench block to quickly initiate actions and jump pages.", diff --git a/packages/plugins/@nocobase/plugin-calendar/package.json b/packages/plugins/@nocobase/plugin-calendar/package.json index 4315dec8d9..bf81ad8eab 100644 --- a/packages/plugins/@nocobase/plugin-calendar/package.json +++ b/packages/plugins/@nocobase/plugin-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-calendar", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Calendar", "displayName.zh-CN": "日历", "description": "Provides callendar collection template and block for managing date data, typically for date/time related information such as events, appointments, tasks, and so on.", diff --git a/packages/plugins/@nocobase/plugin-charts/package.json b/packages/plugins/@nocobase/plugin-charts/package.json index 8784601af4..d2a85c28ce 100644 --- a/packages/plugins/@nocobase/plugin-charts/package.json +++ b/packages/plugins/@nocobase/plugin-charts/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "图表(废弃)", "description": "The plugin has been deprecated, please use the data visualization plugin instead.", "description.zh-CN": "已废弃插件,请使用数据可视化插件代替。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "license": "AGPL-3.0", "devDependencies": { diff --git a/packages/plugins/@nocobase/plugin-client/package.json b/packages/plugins/@nocobase/plugin-client/package.json index b1d0bbbfd8..d60529c745 100644 --- a/packages/plugins/@nocobase/plugin-client/package.json +++ b/packages/plugins/@nocobase/plugin-client/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "WEB 客户端", "description": "Provides a client interface for the NocoBase server", "description.zh-CN": "为 NocoBase 服务端提供客户端界面", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "license": "AGPL-3.0", "devDependencies": { diff --git a/packages/plugins/@nocobase/plugin-collection-sql/package.json b/packages/plugins/@nocobase/plugin-collection-sql/package.json index 8cf1f91ea9..2975d65496 100644 --- a/packages/plugins/@nocobase/plugin-collection-sql/package.json +++ b/packages/plugins/@nocobase/plugin-collection-sql/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表: SQL", "description": "Provides SQL collection template", "description.zh-CN": "提供 SQL 数据表模板", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "homepage": "https://docs-cn.nocobase.com/handbook/collection-sql", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/collection-sql", "main": "dist/server/index.js", diff --git a/packages/plugins/@nocobase/plugin-collection-tree/package.json b/packages/plugins/@nocobase/plugin-collection-tree/package.json index d948bc0a82..05406659b2 100644 --- a/packages/plugins/@nocobase/plugin-collection-tree/package.json +++ b/packages/plugins/@nocobase/plugin-collection-tree/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-collection-tree", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Collection: Tree", "displayName.zh-CN": "数据表:树", "description": "Provides tree collection template", diff --git a/packages/plugins/@nocobase/plugin-data-source-main/package.json b/packages/plugins/@nocobase/plugin-data-source-main/package.json index 8f52ebb40d..7b51d7eb01 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-main/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据源:主数据库", "description": "NocoBase main database, supports relational databases such as MySQL, PostgreSQL, SQLite and so on.", "description.zh-CN": "NocoBase 主数据库,支持 MySQL、PostgreSQL、SQLite 等关系型数据库。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/data-source-main", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/data-source-main", diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/package.json b/packages/plugins/@nocobase/plugin-data-source-manager/package.json index 3271228b7c..a2e72d6ce7 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/package.json +++ b/packages/plugins/@nocobase/plugin-data-source-manager/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-data-source-manager", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "displayName": "Data source manager", "displayName.zh-CN": "数据源管理", diff --git a/packages/plugins/@nocobase/plugin-data-visualization/package.json b/packages/plugins/@nocobase/plugin-data-visualization/package.json index be41e3a3e5..ee1af88378 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/package.json +++ b/packages/plugins/@nocobase/plugin-data-visualization/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-data-visualization", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Data visualization", "displayName.zh-CN": "数据可视化", "description": "Provides data visualization feature, including chart block and chart filter block, support line charts, area charts, bar charts and more than a dozen kinds of charts, you can also extend more chart types.", diff --git a/packages/plugins/@nocobase/plugin-disable-pm-add/package.json b/packages/plugins/@nocobase/plugin-disable-pm-add/package.json index 9aee732a06..b1f6d352ae 100644 --- a/packages/plugins/@nocobase/plugin-disable-pm-add/package.json +++ b/packages/plugins/@nocobase/plugin-disable-pm-add/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-disable-pm-add", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "peerDependencies": { "@nocobase/client": "1.x", diff --git a/packages/plugins/@nocobase/plugin-error-handler/package.json b/packages/plugins/@nocobase/plugin-error-handler/package.json index b2d5899b2f..2ec5237125 100644 --- a/packages/plugins/@nocobase/plugin-error-handler/package.json +++ b/packages/plugins/@nocobase/plugin-error-handler/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "错误处理器", "description": "Handling application errors and exceptions.", "description.zh-CN": "处理应用程序中的错误和异常。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "devDependencies": { diff --git a/packages/plugins/@nocobase/plugin-field-china-region/package.json b/packages/plugins/@nocobase/plugin-field-china-region/package.json index ddf8a8d452..dd7846d4d8 100644 --- a/packages/plugins/@nocobase/plugin-field-china-region/package.json +++ b/packages/plugins/@nocobase/plugin-field-china-region/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-field-china-region", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Administrative divisions of China", "displayName.zh-CN": "中国行政区划", "description": "Provides data and field type for administrative divisions of China.", diff --git a/packages/plugins/@nocobase/plugin-field-formula/package.json b/packages/plugins/@nocobase/plugin-field-formula/package.json index 4e46461758..c1bee2cc8f 100644 --- a/packages/plugins/@nocobase/plugin-field-formula/package.json +++ b/packages/plugins/@nocobase/plugin-field-formula/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表字段:公式", "description": "Configure and store the results of calculations between multiple field values in the same record, supporting both Math.js and Excel formula functions.", "description.zh-CN": "可以配置并存储同一条记录的多字段值之间的计算结果,支持 Math.js 和 Excel formula functions 两种引擎", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/field-formula", diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/package.json b/packages/plugins/@nocobase/plugin-field-m2m-array/package.json index 503be69a1f..150fb553f7 100644 --- a/packages/plugins/@nocobase/plugin-field-m2m-array/package.json +++ b/packages/plugins/@nocobase/plugin-field-m2m-array/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表字段:多对多 (数组)", "description": "Allows to create many to many relationships between two models by storing an array of unique keys of the target model.", "description.zh-CN": "支持通过在数组中存储目标表唯一键的方式建立多对多关系。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "peerDependencies": { "@nocobase/client": "1.x", diff --git a/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json b/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json index ad8085cf95..bc3b98ee84 100644 --- a/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json +++ b/packages/plugins/@nocobase/plugin-field-markdown-vditor/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表字段:Markdown(Vditor)", "description": "Used to store Markdown and render it using Vditor editor, supports common Markdown syntax such as list, code, quote, etc., and supports uploading images, recordings, etc.It also allows for instant rendering, where what you see is what you get.", "description.zh-CN": "用于存储 Markdown,并使用 Vditor 编辑器渲染,支持常见 Markdown 语法,如列表,代码,引用等,并支持上传图片,录音等。同时可以做到即时渲染,所见即所得。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/field-markdown-vditor", diff --git a/packages/plugins/@nocobase/plugin-field-sequence/package.json b/packages/plugins/@nocobase/plugin-field-sequence/package.json index 3581932e08..43d0a5e7ce 100644 --- a/packages/plugins/@nocobase/plugin-field-sequence/package.json +++ b/packages/plugins/@nocobase/plugin-field-sequence/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表字段:自动编码", "description": "Automatically generate codes based on configured rules, supporting combinations of dates, numbers, and text.", "description.zh-CN": "根据配置的规则自动生成编码,支持日期、数字、文本的组合。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/field-sequence", diff --git a/packages/plugins/@nocobase/plugin-file-manager/package.json b/packages/plugins/@nocobase/plugin-file-manager/package.json index 439627ffc0..e040c4edd8 100644 --- a/packages/plugins/@nocobase/plugin-file-manager/package.json +++ b/packages/plugins/@nocobase/plugin-file-manager/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-file-manager", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "File manager", "displayName.zh-CN": "文件管理器", "description": "Provides files storage services with files collection template and attachment field.", diff --git a/packages/plugins/@nocobase/plugin-gantt/package.json b/packages/plugins/@nocobase/plugin-gantt/package.json index b50dced1b0..c727dc0393 100644 --- a/packages/plugins/@nocobase/plugin-gantt/package.json +++ b/packages/plugins/@nocobase/plugin-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-gantt", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "displayName": "Block: Gantt", "displayName.zh-CN": "区块:甘特图", "description": "Provides Gantt block.", diff --git a/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json b/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json index 20763f7d44..cdaffd49fb 100644 --- a/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json +++ b/packages/plugins/@nocobase/plugin-graph-collection-manager/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "可视化数据表管理", "description": "An ER diagram-like tool. Currently only the Master database is supported.", "description.zh-CN": "类似 ER 图的工具,目前只支持主数据库。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/graph-collection-manager", diff --git a/packages/plugins/@nocobase/plugin-kanban/package.json b/packages/plugins/@nocobase/plugin-kanban/package.json index 717e59d152..76dd671ae2 100644 --- a/packages/plugins/@nocobase/plugin-kanban/package.json +++ b/packages/plugins/@nocobase/plugin-kanban/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-kanban", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/block-kanban", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-kanban", diff --git a/packages/plugins/@nocobase/plugin-localization/package.json b/packages/plugins/@nocobase/plugin-localization/package.json index 76f9659aa9..e9193d9170 100644 --- a/packages/plugins/@nocobase/plugin-localization/package.json +++ b/packages/plugins/@nocobase/plugin-localization/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-localization", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/localization-management", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/localization-management", diff --git a/packages/plugins/@nocobase/plugin-logger/package.json b/packages/plugins/@nocobase/plugin-logger/package.json index 23646ffbdf..b4430b410e 100644 --- a/packages/plugins/@nocobase/plugin-logger/package.json +++ b/packages/plugins/@nocobase/plugin-logger/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "日志", "description": "Server-side logs, mainly including API request logs and system runtime logs, and allows to package and download log files.", "description.zh-CN": "服务端日志,主要包括接口请求日志和系统运行日志,并支持打包和下载日志文件。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/logger", diff --git a/packages/plugins/@nocobase/plugin-map/package.json b/packages/plugins/@nocobase/plugin-map/package.json index 13a33fb533..77b4249cd2 100644 --- a/packages/plugins/@nocobase/plugin-map/package.json +++ b/packages/plugins/@nocobase/plugin-map/package.json @@ -2,7 +2,7 @@ "name": "@nocobase/plugin-map", "displayName": "Block: Map", "displayName.zh-CN": "区块:地图", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "Map block, support Gaode map and Google map, you can also extend more map types.", "description.zh-CN": "地图区块,支持高德地图和 Google 地图,你也可以扩展更多地图类型。", "license": "AGPL-3.0", diff --git a/packages/plugins/@nocobase/plugin-mobile-client/package.json b/packages/plugins/@nocobase/plugin-mobile-client/package.json index 82bf4429cc..4a3dedbd6a 100644 --- a/packages/plugins/@nocobase/plugin-mobile-client/package.json +++ b/packages/plugins/@nocobase/plugin-mobile-client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-mobile-client", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/mobile-client", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/mobile-client", diff --git a/packages/plugins/@nocobase/plugin-mobile/package.json b/packages/plugins/@nocobase/plugin-mobile/package.json index 4bffed2079..869253975c 100644 --- a/packages/plugins/@nocobase/plugin-mobile/package.json +++ b/packages/plugins/@nocobase/plugin-mobile/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-mobile", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/mobile", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/mobile", diff --git a/packages/plugins/@nocobase/plugin-mock-collections/package.json b/packages/plugins/@nocobase/plugin-mock-collections/package.json index 493a31c798..fb5e033d77 100644 --- a/packages/plugins/@nocobase/plugin-mock-collections/package.json +++ b/packages/plugins/@nocobase/plugin-mock-collections/package.json @@ -2,7 +2,7 @@ "name": "@nocobase/plugin-mock-collections", "displayName": "mock-collections", "description": "mock-collections", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "license": "AGPL-3.0", "peerDependencies": { diff --git a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json index 8c50eab224..af14f04dfb 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-manager/package.json +++ b/packages/plugins/@nocobase/plugin-multi-app-manager/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "多应用管理器", "description": "Dynamically create multiple apps without separate deployments.", "description.zh-CN": "无需单独部署即可动态创建多个应用。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/multi-app-manager", diff --git a/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json b/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json index 03b6b8ae96..cfc6f33d90 100644 --- a/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json +++ b/packages/plugins/@nocobase/plugin-multi-app-share-collection/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "多应用数据表共享", "description": "", "description.zh-CN": "", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "devDependencies": { "@formily/react": "2.x", diff --git a/packages/plugins/@nocobase/plugin-notifications/package.json b/packages/plugins/@nocobase/plugin-notifications/package.json index 690f5f902d..c1b7e0e084 100644 --- a/packages/plugins/@nocobase/plugin-notifications/package.json +++ b/packages/plugins/@nocobase/plugin-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-notifications", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "description": "", "license": "AGPL-3.0", "main": "./dist/server/index.js", diff --git a/packages/plugins/@nocobase/plugin-sample-hello/package.json b/packages/plugins/@nocobase/plugin-sample-hello/package.json index a63809cde4..5e1c4ec23c 100644 --- a/packages/plugins/@nocobase/plugin-sample-hello/package.json +++ b/packages/plugins/@nocobase/plugin-sample-hello/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-sample-hello", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "./dist/server/index.js", "displayName": "Hello", "displayName.zh-CN": "Hello", diff --git a/packages/plugins/@nocobase/plugin-snapshot-field/package.json b/packages/plugins/@nocobase/plugin-snapshot-field/package.json index d2d424aa18..30a9010033 100644 --- a/packages/plugins/@nocobase/plugin-snapshot-field/package.json +++ b/packages/plugins/@nocobase/plugin-snapshot-field/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "数据表字段:关系快照", "description": "When adding a new record, create a snapshot for its relational record and save in the new record. The snapshot will not be updated when the relational record is updated.", "description.zh-CN": "在添加数据时,为它的关系数据创建快照,并保存在当前的数据中。关系数据更新时,快照不会更新。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/field-snapshot", diff --git a/packages/plugins/@nocobase/plugin-system-settings/package.json b/packages/plugins/@nocobase/plugin-system-settings/package.json index a506eaa86f..f3b05fd5ea 100644 --- a/packages/plugins/@nocobase/plugin-system-settings/package.json +++ b/packages/plugins/@nocobase/plugin-system-settings/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "系统设置", "description": "Used to adjust the system title, logo, language, etc.", "description.zh-CN": "用于调整系统的标题、LOGO、语言等。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/system-settings", diff --git a/packages/plugins/@nocobase/plugin-theme-editor/package.json b/packages/plugins/@nocobase/plugin-theme-editor/package.json index 54a87780e0..6a0a0172ac 100644 --- a/packages/plugins/@nocobase/plugin-theme-editor/package.json +++ b/packages/plugins/@nocobase/plugin-theme-editor/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/plugin-theme-editor", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "main": "dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/theme-editor", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/theme-editor", diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json b/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json index 733208a370..0f03f503a5 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "UI schema 存储服务", "description": "Provides centralized UI schema storage service.", "description.zh-CN": "提供中心化的 UI schema 存储服务。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/ui-schema-storage", diff --git a/packages/plugins/@nocobase/plugin-users/package.json b/packages/plugins/@nocobase/plugin-users/package.json index 3913a7f1ff..de694f975f 100644 --- a/packages/plugins/@nocobase/plugin-users/package.json +++ b/packages/plugins/@nocobase/plugin-users/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "用户", "description": "Provides basic user model, as well as created by and updated by fields.", "description.zh-CN": "提供了基础的用户模型,以及创建人和最后更新人字段。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/users", diff --git a/packages/plugins/@nocobase/plugin-verification/package.json b/packages/plugins/@nocobase/plugin-verification/package.json index eb5070dac3..ee6af902f7 100644 --- a/packages/plugins/@nocobase/plugin-verification/package.json +++ b/packages/plugins/@nocobase/plugin-verification/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "验证码", "description": "verification setting.", "description.zh-CN": "验证码配置。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/verification", diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json b/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json index a7749f1931..e28708cfe6 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:操作后事件", "description": "Triggered after the completion of a request initiated through an action button or API, such as after adding, updating, deleting data, or \"submit to workflow\". Suitable for data processing, sending notifications, etc., after actions are completed.", "description.zh-CN": "通过操作按钮或 API 发起请求并在执行完成后触发,比如新增、更新、删除数据或者“提交至工作流”之后。适用于在操作完成后进行数据处理、发送通知等。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/plugins/workflow-action-trigger", diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json b/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json index aeca81a7a5..8e85d598f3 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:聚合查询节点", "description": "Used to aggregate data against the database in workflow, such as: statistics, sum, average, etc.", "description.zh-CN": "可用于在工作流中对数据库进行聚合查询,如:统计数量、求和、平均值等。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-aggregate", diff --git a/packages/plugins/@nocobase/plugin-workflow-delay/package.json b/packages/plugins/@nocobase/plugin-workflow-delay/package.json index badefe6cfa..d27b5efc88 100644 --- a/packages/plugins/@nocobase/plugin-workflow-delay/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-delay/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:延时节点", "description": "Could be used in workflow parallel branch for waiting other branches.", "description.zh-CN": "可用于工作流并行分支中等待其他分支执行完成。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-delay", diff --git a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json index 9d55a2d574..431a9f48e0 100644 --- a/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-dynamic-calculation/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:动态表达式计算节点", "description": "Useful plugin for doing dynamic calculation based on expression collection records in workflow.", "description.zh-CN": "用于在工作流中进行基于数据行的动态表达式计算。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-dynamic-calculation", diff --git a/packages/plugins/@nocobase/plugin-workflow-loop/package.json b/packages/plugins/@nocobase/plugin-workflow-loop/package.json index aa92a382f2..0c03dfd8b7 100644 --- a/packages/plugins/@nocobase/plugin-workflow-loop/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-loop/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:循环节点", "description": "Used to repeat the sub-process processing of each value in an array, and can also be used for fixed times of sub-process processing.", "description.zh-CN": "用于对一个数组中的每个值进行重复的子流程处理,也可用于固定次数的重复子流程处理。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-loop", diff --git a/packages/plugins/@nocobase/plugin-workflow-mailer/package.json b/packages/plugins/@nocobase/plugin-workflow-mailer/package.json index c6506f4f2b..e18835910b 100644 --- a/packages/plugins/@nocobase/plugin-workflow-mailer/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-mailer/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:邮件发送节点", "description": "Send email in workflow.", "description.zh-CN": "可用于在工作流中发送电子邮件。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-smtp-mailer", diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/package.json b/packages/plugins/@nocobase/plugin-workflow-manual/package.json index bb30aeee74..05a40dd5b5 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-manual/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:人工处理节点", "description": "Could be used for workflows which some of decisions are made by users.", "description.zh-CN": "用于人工控制部分决策的流程。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-manual", diff --git a/packages/plugins/@nocobase/plugin-workflow-parallel/package.json b/packages/plugins/@nocobase/plugin-workflow-parallel/package.json index 9cb3827a0d..4493bff682 100644 --- a/packages/plugins/@nocobase/plugin-workflow-parallel/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-parallel/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:并行分支节点", "description": "Could be used for parallel execution of branch processes in the workflow.", "description.zh-CN": "用于在工作流中需要并行执行的分支流程。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-parallel", diff --git a/packages/plugins/@nocobase/plugin-workflow-request/package.json b/packages/plugins/@nocobase/plugin-workflow-request/package.json index 3ea639dfaa..f864fb0630 100644 --- a/packages/plugins/@nocobase/plugin-workflow-request/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-request/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:HTTP 请求节点", "description": "Send HTTP requests to any HTTP service for data interaction in workflow.", "description.zh-CN": "可用于在工作流中向任意 HTTP 服务发送请求,进行数据交互。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-request", diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/package.json b/packages/plugins/@nocobase/plugin-workflow-sql/package.json index d7d7541c2c..facbdd3e88 100644 --- a/packages/plugins/@nocobase/plugin-workflow-sql/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-sql/package.json @@ -4,7 +4,7 @@ "displayName.zh-CN": "工作流:SQL 节点", "description": "Execute SQL statements in workflow.", "description.zh-CN": "可用于在工作流中对数据库执行任意 SQL 语句。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow-sql", diff --git a/packages/plugins/@nocobase/plugin-workflow-test/package.json b/packages/plugins/@nocobase/plugin-workflow-test/package.json index 63e681c3a9..456be85a5f 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/package.json +++ b/packages/plugins/@nocobase/plugin-workflow-test/package.json @@ -2,7 +2,7 @@ "name": "@nocobase/plugin-workflow-test", "displayName": "Workflow: test kit", "displayName.zh-CN": "工作流:测试工具包", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "dist/server/index.js", "types": "./dist/server/index.d.ts", diff --git a/packages/plugins/@nocobase/plugin-workflow/package.json b/packages/plugins/@nocobase/plugin-workflow/package.json index 1100c4cec3..b3ee220cf1 100644 --- a/packages/plugins/@nocobase/plugin-workflow/package.json +++ b/packages/plugins/@nocobase/plugin-workflow/package.json @@ -4,13 +4,13 @@ "displayName.zh-CN": "工作流", "description": "A powerful BPM tool that provides foundational support for business automation, with the capability to extend unlimited triggers and nodes.", "description.zh-CN": "一个强大的 BPM 工具,为业务自动化提供基础支持,并且可任意扩展更多的触发器和节点。", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./dist/server/index.js", "homepage": "https://docs.nocobase.com/handbook/workflow", "homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/workflow", "dependencies": { - "@nocobase/plugin-workflow-test": "1.3.0-beta" + "@nocobase/plugin-workflow-test": "1.4.0-alpha" }, "devDependencies": { "@ant-design/icons": "5.x", diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index fd4bb872b7..5b89f1b465 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -1,70 +1,70 @@ { "name": "@nocobase/preset-nocobase", - "version": "1.3.0-beta", + "version": "1.4.0-alpha", "license": "AGPL-3.0", "main": "./lib/server/index.js", "dependencies": { "@formily/json-schema": "2.x", - "@nocobase/plugin-acl": "1.3.0-beta", - "@nocobase/plugin-action-bulk-edit": "1.3.0-beta", - "@nocobase/plugin-action-bulk-update": "1.3.0-beta", - "@nocobase/plugin-action-custom-request": "1.3.0-beta", - "@nocobase/plugin-action-duplicate": "1.3.0-beta", - "@nocobase/plugin-action-export": "1.3.0-beta", - "@nocobase/plugin-action-import": "1.3.0-beta", - "@nocobase/plugin-action-print": "1.3.0-beta", - "@nocobase/plugin-api-doc": "1.3.0-beta", - "@nocobase/plugin-api-keys": "1.3.0-beta", - "@nocobase/plugin-audit-logs": "1.3.0-beta", - "@nocobase/plugin-auth": "1.3.0-beta", - "@nocobase/plugin-auth-sms": "1.3.0-beta", - "@nocobase/plugin-backup-restore": "1.3.0-beta", - "@nocobase/plugin-block-iframe": "1.3.0-beta", - "@nocobase/plugin-block-workbench": "1.3.0-beta", - "@nocobase/plugin-calendar": "1.3.0-beta", - "@nocobase/plugin-charts": "1.3.0-beta", - "@nocobase/plugin-client": "1.3.0-beta", - "@nocobase/plugin-collection-sql": "1.3.0-beta", - "@nocobase/plugin-collection-tree": "1.3.0-beta", - "@nocobase/plugin-data-source-main": "1.3.0-beta", - "@nocobase/plugin-data-source-manager": "1.3.0-beta", - "@nocobase/plugin-data-visualization": "1.3.0-beta", - "@nocobase/plugin-error-handler": "1.3.0-beta", - "@nocobase/plugin-field-china-region": "1.3.0-beta", - "@nocobase/plugin-field-formula": "1.3.0-beta", - "@nocobase/plugin-field-m2m-array": "1.3.0-beta", - "@nocobase/plugin-field-markdown-vditor": "1.3.0-beta", - "@nocobase/plugin-field-sequence": "1.3.0-beta", - "@nocobase/plugin-file-manager": "1.3.0-beta", - "@nocobase/plugin-gantt": "1.3.0-beta", - "@nocobase/plugin-graph-collection-manager": "1.3.0-beta", - "@nocobase/plugin-kanban": "1.3.0-beta", - "@nocobase/plugin-localization": "1.3.0-beta", - "@nocobase/plugin-logger": "1.3.0-beta", - "@nocobase/plugin-map": "1.3.0-beta", - "@nocobase/plugin-mobile": "1.3.0-beta", - "@nocobase/plugin-mobile-client": "1.3.0-beta", - "@nocobase/plugin-mock-collections": "1.3.0-beta", - "@nocobase/plugin-multi-app-manager": "1.3.0-beta", - "@nocobase/plugin-multi-app-share-collection": "1.3.0-beta", - "@nocobase/plugin-snapshot-field": "1.3.0-beta", - "@nocobase/plugin-system-settings": "1.3.0-beta", - "@nocobase/plugin-theme-editor": "1.3.0-beta", - "@nocobase/plugin-ui-schema-storage": "1.3.0-beta", - "@nocobase/plugin-users": "1.3.0-beta", - "@nocobase/plugin-verification": "1.3.0-beta", - "@nocobase/plugin-workflow": "1.3.0-beta", - "@nocobase/plugin-workflow-action-trigger": "1.3.0-beta", - "@nocobase/plugin-workflow-aggregate": "1.3.0-beta", - "@nocobase/plugin-workflow-delay": "1.3.0-beta", - "@nocobase/plugin-workflow-dynamic-calculation": "1.3.0-beta", - "@nocobase/plugin-workflow-loop": "1.3.0-beta", - "@nocobase/plugin-workflow-mailer": "1.3.0-beta", - "@nocobase/plugin-workflow-manual": "1.3.0-beta", - "@nocobase/plugin-workflow-parallel": "1.3.0-beta", - "@nocobase/plugin-workflow-request": "1.3.0-beta", - "@nocobase/plugin-workflow-sql": "1.3.0-beta", - "@nocobase/server": "1.3.0-beta", + "@nocobase/plugin-acl": "1.4.0-alpha", + "@nocobase/plugin-action-bulk-edit": "1.4.0-alpha", + "@nocobase/plugin-action-bulk-update": "1.4.0-alpha", + "@nocobase/plugin-action-custom-request": "1.4.0-alpha", + "@nocobase/plugin-action-duplicate": "1.4.0-alpha", + "@nocobase/plugin-action-export": "1.4.0-alpha", + "@nocobase/plugin-action-import": "1.4.0-alpha", + "@nocobase/plugin-action-print": "1.4.0-alpha", + "@nocobase/plugin-api-doc": "1.4.0-alpha", + "@nocobase/plugin-api-keys": "1.4.0-alpha", + "@nocobase/plugin-audit-logs": "1.4.0-alpha", + "@nocobase/plugin-auth": "1.4.0-alpha", + "@nocobase/plugin-auth-sms": "1.4.0-alpha", + "@nocobase/plugin-backup-restore": "1.4.0-alpha", + "@nocobase/plugin-block-iframe": "1.4.0-alpha", + "@nocobase/plugin-block-workbench": "1.4.0-alpha", + "@nocobase/plugin-calendar": "1.4.0-alpha", + "@nocobase/plugin-charts": "1.4.0-alpha", + "@nocobase/plugin-client": "1.4.0-alpha", + "@nocobase/plugin-collection-sql": "1.4.0-alpha", + "@nocobase/plugin-collection-tree": "1.4.0-alpha", + "@nocobase/plugin-data-source-main": "1.4.0-alpha", + "@nocobase/plugin-data-source-manager": "1.4.0-alpha", + "@nocobase/plugin-data-visualization": "1.4.0-alpha", + "@nocobase/plugin-error-handler": "1.4.0-alpha", + "@nocobase/plugin-field-china-region": "1.4.0-alpha", + "@nocobase/plugin-field-formula": "1.4.0-alpha", + "@nocobase/plugin-field-m2m-array": "1.4.0-alpha", + "@nocobase/plugin-field-markdown-vditor": "1.4.0-alpha", + "@nocobase/plugin-field-sequence": "1.4.0-alpha", + "@nocobase/plugin-file-manager": "1.4.0-alpha", + "@nocobase/plugin-gantt": "1.4.0-alpha", + "@nocobase/plugin-graph-collection-manager": "1.4.0-alpha", + "@nocobase/plugin-kanban": "1.4.0-alpha", + "@nocobase/plugin-localization": "1.4.0-alpha", + "@nocobase/plugin-logger": "1.4.0-alpha", + "@nocobase/plugin-map": "1.4.0-alpha", + "@nocobase/plugin-mobile": "1.4.0-alpha", + "@nocobase/plugin-mobile-client": "1.4.0-alpha", + "@nocobase/plugin-mock-collections": "1.4.0-alpha", + "@nocobase/plugin-multi-app-manager": "1.4.0-alpha", + "@nocobase/plugin-multi-app-share-collection": "1.4.0-alpha", + "@nocobase/plugin-snapshot-field": "1.4.0-alpha", + "@nocobase/plugin-system-settings": "1.4.0-alpha", + "@nocobase/plugin-theme-editor": "1.4.0-alpha", + "@nocobase/plugin-ui-schema-storage": "1.4.0-alpha", + "@nocobase/plugin-users": "1.4.0-alpha", + "@nocobase/plugin-verification": "1.4.0-alpha", + "@nocobase/plugin-workflow": "1.4.0-alpha", + "@nocobase/plugin-workflow-action-trigger": "1.4.0-alpha", + "@nocobase/plugin-workflow-aggregate": "1.4.0-alpha", + "@nocobase/plugin-workflow-delay": "1.4.0-alpha", + "@nocobase/plugin-workflow-dynamic-calculation": "1.4.0-alpha", + "@nocobase/plugin-workflow-loop": "1.4.0-alpha", + "@nocobase/plugin-workflow-mailer": "1.4.0-alpha", + "@nocobase/plugin-workflow-manual": "1.4.0-alpha", + "@nocobase/plugin-workflow-parallel": "1.4.0-alpha", + "@nocobase/plugin-workflow-request": "1.4.0-alpha", + "@nocobase/plugin-workflow-sql": "1.4.0-alpha", + "@nocobase/server": "1.4.0-alpha", "cronstrue": "^2.11.0" }, "repository": { From 2c757c43c5946c106f6f699269026ecd20e47f1d Mon Sep 17 00:00:00 2001 From: Zhi Chen <350447433@qq.com> Date: Tue, 27 Aug 2024 05:25:30 +0800 Subject: [PATCH 02/68] feat: user data sync plugin (#4986) * feat: user data sync plugin * fix: nickname bug * feat: adjust sync * fix: delete package-lock.json * feat: adjust push * feat: adjust user-data-sync plugin * fix: delete submodule * fix: sync bug * fix: adjust log * fix: adjust log * chore: add to build-in * chore: update version * chore: add api doc * chore: add plugin description * chore: api doc * chore: change keywords * feat: add no source type prompt * chore: text to json * feat: optimize resource manager * fix: test & typo * test: add tests for resource manager & fix bug * fix: user sync * chore: remove department resource * fix: test * fix: build * fix: push data * fix: push api add default sourceName * test: add api test * fix: uniqueKey support any * chore: single user belongs to multi department situation * chore: department * fix: fix sync bugs * fix: sync departments * chore: remove department logic * chore: remove department * fix: fix logger type error * fix: fix sync bug * fix: logger & role * fix: fix sync bugs * fix: fix sync bug * fix: fix sync bugs * test: update test cases * chore: update * chore: update * fix: test * fix: test * fix: test * fix: bugs * fix: version --------- Co-authored-by: chenzhi Co-authored-by: xilesun <2013xile@gmail.com> --- .../plugin-user-data-sync/.npmignore | 2 + .../@nocobase/plugin-user-data-sync/README.md | 1 + .../plugin-user-data-sync/client.d.ts | 2 + .../@nocobase/plugin-user-data-sync/client.js | 1 + .../plugin-user-data-sync/package.json | 18 + .../plugin-user-data-sync/server.d.ts | 2 + .../@nocobase/plugin-user-data-sync/server.js | 1 + .../src/client/Options.tsx | 54 ++ .../src/client/UserDataSyncSource.tsx | 284 ++++++++++ .../src/client/client.d.ts | 249 +++++++++ .../src/client/index.tsx | 41 ++ .../src/client/locale/index.ts | 16 + .../client/schemas/user-data-sync-sources.ts | 523 ++++++++++++++++++ .../src/client/sourceType.ts | 27 + .../plugin-user-data-sync/src/index.ts | 11 + .../src/locale/en-US.json | 19 + .../src/locale/zh-CN.json | 22 + .../src/server/__tests__/api.test.ts | 54 ++ .../src/server/__tests__/mock-resource.ts | 41 ++ .../server/__tests__/resource-manager.test.ts | 153 +++++ .../src/server/actions/user-data.ts | 44 ++ .../src/server/collections/.gitkeep | 0 .../user-data-sync-records-resources.ts | 33 ++ .../collections/user-data-sync-records.ts | 64 +++ .../collections/user-data-sync-sources.ts | 88 +++ .../collections/user-data-sync-tasks.ts | 110 ++++ .../plugin-user-data-sync/src/server/index.ts | 12 + .../src/server/models/sync-source.ts | 17 + .../src/server/plugin.ts | 71 +++ .../src/server/sync-source-manager.ts | 58 ++ .../src/server/sync-source.ts | 106 ++++ .../src/server/user-data-resource-manager.ts | 268 +++++++++ .../src/server/user-data-sync-service.ts | 111 ++++ .../src/swagger/index.ts | 121 ++++ .../@nocobase/plugin-users/package.json | 1 + .../src/server/__tests__/data-sync.test.ts | 122 ++++ .../plugin-users/src/server/server.ts | 8 +- .../src/server/user-data-sync-resource.ts | 102 ++++ packages/presets/nocobase/package.json | 1 + packages/presets/nocobase/src/server/index.ts | 1 + 40 files changed, 2858 insertions(+), 1 deletion(-) create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/.npmignore create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/README.md create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/client.js create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/package.json create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/server.js create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/Options.tsx create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/UserDataSyncSource.tsx create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/index.tsx create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/locale/index.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/schemas/user-data-sync-sources.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/client/sourceType.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/index.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/locale/en-US.json create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/locale/zh-CN.json create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/mock-resource.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/actions/user-data.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/.gitkeep create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records-resources.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-sources.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-tasks.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/index.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/models/sync-source.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source-manager.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-sync-service.ts create mode 100644 packages/plugins/@nocobase/plugin-user-data-sync/src/swagger/index.ts create mode 100644 packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts create mode 100644 packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/.npmignore b/packages/plugins/@nocobase/plugin-user-data-sync/.npmignore new file mode 100644 index 0000000000..65f5e8779f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/.npmignore @@ -0,0 +1,2 @@ +/node_modules +/src diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/README.md b/packages/plugins/@nocobase/plugin-user-data-sync/README.md new file mode 100644 index 0000000000..13dcf98789 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/README.md @@ -0,0 +1 @@ +# @nocobase/plugin-user-data-sync diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts b/packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts new file mode 100644 index 0000000000..6c459cbac4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts @@ -0,0 +1,2 @@ +export * from './dist/client'; +export { default } from './dist/client'; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/client.js b/packages/plugins/@nocobase/plugin-user-data-sync/client.js new file mode 100644 index 0000000000..b6e3be70e6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/package.json b/packages/plugins/@nocobase/plugin-user-data-sync/package.json new file mode 100644 index 0000000000..039d36eefc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/package.json @@ -0,0 +1,18 @@ +{ + "name": "@nocobase/plugin-user-data-sync", + "displayName": "User data synchronization", + "displayName.zh-CN": "用户数据同步", + "description": "Provide user data source management and user data synchronization interface. The data source can be DingTalk, WeCom, etc., and can be expanded.", + "description.zh-CN": "提供用户数据源管理,用户数据同步接口,数据源可为钉钉、企业微信等,可扩展。", + "version": "1.4.0-alpha", + "main": "dist/server/index.js", + "dependencies": {}, + "peerDependencies": { + "@nocobase/client": "1.x", + "@nocobase/server": "1.x", + "@nocobase/test": "1.x" + }, + "keywords": [ + "Users & permissions" + ] +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts b/packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts new file mode 100644 index 0000000000..c41081ddc6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts @@ -0,0 +1,2 @@ +export * from './dist/server'; +export { default } from './dist/server'; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/server.js b/packages/plugins/@nocobase/plugin-user-data-sync/server.js new file mode 100644 index 0000000000..972842039a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/Options.tsx b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/Options.tsx new file mode 100644 index 0000000000..eb5070ed6d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/Options.tsx @@ -0,0 +1,54 @@ +/** + * 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 React from 'react'; +import { observer, useForm } from '@formily/react'; +import { useActionContext, useCollectionRecordData, usePlugin, useRequest } from '@nocobase/client'; +import { useEffect } from 'react'; +import SourcePlugin from '.'; + +export const useValuesFromOptions = (options) => { + const record = useCollectionRecordData(); + const result = useRequest( + () => + Promise.resolve({ + data: { + ...record.options, + }, + }), + { + ...options, + manual: true, + }, + ); + const { run } = result; + const ctx = useActionContext(); + useEffect(() => { + if (ctx.visible) { + run(); + } + }, [ctx.visible, run]); + return result; +}; + +export const useAdminSettingsForm = (sourceType: string) => { + const plugin = usePlugin(SourcePlugin); + const source = plugin.sourceTypes.get(sourceType); + return source?.components?.AdminSettingsForm; +}; + +export const Options = observer( + () => { + const form = useForm(); + const record = useCollectionRecordData(); + const Component = useAdminSettingsForm(form.values.sourceType || record.sourceType); + return Component ? : null; + }, + { displayName: 'Options' }, +); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/UserDataSyncSource.tsx b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/UserDataSyncSource.tsx new file mode 100644 index 0000000000..83c35bcd08 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/UserDataSyncSource.tsx @@ -0,0 +1,284 @@ +/** + * 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 { + ActionContextProvider, + SchemaComponent, + useAPIClient, + useActionContext, + useRequest, + ExtendCollectionsProvider, + useDataBlockRequest, + useDataBlockResource, + useCollection, + useCollectionRecordData, + ActionProps, +} from '@nocobase/client'; +import { App as AntdApp } from 'antd'; +import React, { useContext, useMemo, useState } from 'react'; +import { + userDataSyncSourcesSchema, + createFormSchema, + sourceCollection, + tasksTableBlockSchema, +} from './schemas/user-data-sync-sources'; +import { Button, Dropdown, Empty } from 'antd'; +import { PlusOutlined, DownOutlined } from '@ant-design/icons'; +import { SourceTypeContext, SourceTypesContext, useSourceTypes } from './sourceType'; +import { useValuesFromOptions, Options } from './Options'; +import { NAMESPACE, useUserDataSyncSourceTranslation } from './locale'; +import { Schema, useForm } from '@formily/react'; +import { taskCollection } from './schemas/user-data-sync-sources'; +import { createForm } from '@formily/core'; + +const useEditFormProps = () => { + const recordData = useCollectionRecordData(); + const form = useMemo( + () => + createForm({ + values: recordData, + }), + [], + ); + + return { + form, + }; +}; + +const useSubmitActionProps = () => { + const { setVisible } = useActionContext(); + const { message } = AntdApp.useApp(); + const form = useForm(); + const resource = useDataBlockResource(); + const { runAsync } = useDataBlockRequest(); + const collection = useCollection(); + + return { + type: 'primary', + async onClick() { + await form.submit(); + const values = form.values; + if (values[collection.filterTargetKey]) { + await resource.update({ + values, + filterByTk: values[collection.filterTargetKey], + }); + } else { + await resource.create({ values }); + } + await runAsync(); + message.success('Saved successfully'); + setVisible(false); + }, + }; +}; + +function useDeleteActionProps(): ActionProps { + const { message } = AntdApp.useApp(); + const record = useCollectionRecordData(); + const resource = useDataBlockResource(); + const collection = useCollection(); + const { runAsync } = useDataBlockRequest(); + return { + confirm: { + title: 'Delete', + content: 'Are you sure you want to delete it?', + }, + async onClick() { + await resource.destroy({ + filterByTk: record[collection.filterTargetKey], + }); + await runAsync(); + message.success('Deleted!'); + }, + }; +} + +function useSyncActionProps(): ActionProps { + const { message } = AntdApp.useApp(); + const record = useCollectionRecordData(); + const api = useAPIClient(); + const { runAsync } = useDataBlockRequest(); + return { + async onClick() { + await api.resource('userData').pull({ name: record['name'] }); + await runAsync(); + message.success('Synced!'); + }, + }; +} + +const useCustomFormProps = () => { + const { type: sourceType } = useContext(SourceTypeContext); + const form = useMemo( + () => + createForm({ + initialValues: { + sourceType: sourceType, + }, + }), + [], + ); + return { + form, + }; +}; + +const useTasksTableBlockProps = () => { + const record = useCollectionRecordData(); + const collection = useCollection(); + return { + params: { + pageSize: 20, + filter: { + sourceId: record[collection.filterTargetKey], + }, + sort: ['-sort'], + }, + }; +}; + +function useRetryActionProps(): ActionProps { + const { message } = AntdApp.useApp(); + const record = useCollectionRecordData(); + const resource = useDataBlockResource(); + const collection = useCollection(); + const api = useAPIClient(); + const { runAsync } = useDataBlockRequest(); + return { + async onClick() { + await api.resource('userData').retry({ id: record[collection.filterTargetKey], sourceId: record['sourceId'] }); + await runAsync(); + message.success('Successfully'); + }, + }; +} + +const AddNew = () => { + const { t } = useUserDataSyncSourceTranslation(); + const api = useAPIClient(); + const [visible, setVisible] = useState(false); + const [type, setType] = useState(''); + const types = useSourceTypes(); + const items = types.map((item) => ({ + ...item, + onClick: () => { + setVisible(true); + setType(item.value); + }, + })); + + const emptyItem = [ + { + key: '__empty__', + label: ( + + {t('No user data source plugin installed', { ns: NAMESPACE })} +
{' '} + + {t('View documentation', { ns: NAMESPACE })} + + + } + /> + ), + onClick: () => {}, + }, + ]; + + return ( + + + 0 ? items : emptyItem }}> + + + + + + ); +}; + +const Tasks = () => { + const { t } = useUserDataSyncSourceTranslation(); + const [visible, setVisible] = useState(false); + return ( + + + + + + + ); +}; + +export const UserDataSyncSource: React.FC = () => { + const { t } = useUserDataSyncSourceTranslation(); + const [types, setTypes] = useState([]); + const api = useAPIClient(); + useRequest( + () => + api + .resource('userData') + .listSyncTypes() + .then((res) => { + const types = res?.data?.data || []; + return types.map((type: { name: string; title?: string }) => ({ + key: type.name, + label: Schema.compile(type.title || type.name, { t }), + value: type.name, + })); + }), + { + onSuccess: (types) => { + setTypes(types); + }, + }, + ); + + return ( + + + + + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts new file mode 100644 index 0000000000..4e96f83fa1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts @@ -0,0 +1,249 @@ +/** + * 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. + */ + +// CSS modules +type CSSModuleClasses = { readonly [key: string]: string }; + +declare module '*.module.css' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.scss' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.sass' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.less' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.styl' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.stylus' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.pcss' { + const classes: CSSModuleClasses; + export default classes; +} +declare module '*.module.sss' { + const classes: CSSModuleClasses; + export default classes; +} + +// CSS +declare module '*.css' { } +declare module '*.scss' { } +declare module '*.sass' { } +declare module '*.less' { } +declare module '*.styl' { } +declare module '*.stylus' { } +declare module '*.pcss' { } +declare module '*.sss' { } + +// Built-in asset types +// see `src/node/constants.ts` + +// images +declare module '*.apng' { + const src: string; + export default src; +} +declare module '*.png' { + const src: string; + export default src; +} +declare module '*.jpg' { + const src: string; + export default src; +} +declare module '*.jpeg' { + const src: string; + export default src; +} +declare module '*.jfif' { + const src: string; + export default src; +} +declare module '*.pjpeg' { + const src: string; + export default src; +} +declare module '*.pjp' { + const src: string; + export default src; +} +declare module '*.gif' { + const src: string; + export default src; +} +declare module '*.svg' { + const src: string; + export default src; +} +declare module '*.ico' { + const src: string; + export default src; +} +declare module '*.webp' { + const src: string; + export default src; +} +declare module '*.avif' { + const src: string; + export default src; +} + +// media +declare module '*.mp4' { + const src: string; + export default src; +} +declare module '*.webm' { + const src: string; + export default src; +} +declare module '*.ogg' { + const src: string; + export default src; +} +declare module '*.mp3' { + const src: string; + export default src; +} +declare module '*.wav' { + const src: string; + export default src; +} +declare module '*.flac' { + const src: string; + export default src; +} +declare module '*.aac' { + const src: string; + export default src; +} +declare module '*.opus' { + const src: string; + export default src; +} +declare module '*.mov' { + const src: string; + export default src; +} +declare module '*.m4a' { + const src: string; + export default src; +} +declare module '*.vtt' { + const src: string; + export default src; +} + +// fonts +declare module '*.woff' { + const src: string; + export default src; +} +declare module '*.woff2' { + const src: string; + export default src; +} +declare module '*.eot' { + const src: string; + export default src; +} +declare module '*.ttf' { + const src: string; + export default src; +} +declare module '*.otf' { + const src: string; + export default src; +} + +// other +declare module '*.webmanifest' { + const src: string; + export default src; +} +declare module '*.pdf' { + const src: string; + export default src; +} +declare module '*.txt' { + const src: string; + export default src; +} + +// wasm?init +declare module '*.wasm?init' { + const initWasm: (options?: WebAssembly.Imports) => Promise; + export default initWasm; +} + +// web worker +declare module '*?worker' { + const workerConstructor: { + new(options?: { name?: string }): Worker; + }; + export default workerConstructor; +} + +declare module '*?worker&inline' { + const workerConstructor: { + new(options?: { name?: string }): Worker; + }; + export default workerConstructor; +} + +declare module '*?worker&url' { + const src: string; + export default src; +} + +declare module '*?sharedworker' { + const sharedWorkerConstructor: { + new(options?: { name?: string }): SharedWorker; + }; + export default sharedWorkerConstructor; +} + +declare module '*?sharedworker&inline' { + const sharedWorkerConstructor: { + new(options?: { name?: string }): SharedWorker; + }; + export default sharedWorkerConstructor; +} + +declare module '*?sharedworker&url' { + const src: string; + export default src; +} + +declare module '*?raw' { + const src: string; + export default src; +} + +declare module '*?url' { + const src: string; + export default src; +} + +declare module '*?inline' { + const src: string; + export default src; +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/index.tsx b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/index.tsx new file mode 100644 index 0000000000..2d6b47cd4e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/index.tsx @@ -0,0 +1,41 @@ +/** + * 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 { Plugin } from '@nocobase/client'; +import { Registry, tval } from '@nocobase/utils/client'; +import { ComponentType } from 'react'; +import { NAMESPACE } from './locale'; +import { UserDataSyncSource } from './UserDataSyncSource'; + +export type SourceOptions = { + components: Partial<{ + AdminSettingsForm: ComponentType; + }>; +}; + +export class PluginUserDataSyncClient extends Plugin { + sourceTypes = new Registry(); + + registerType(sourceType: string, options: SourceOptions) { + this.sourceTypes.register(sourceType, options); + } + + // You can get and modify the app instance here + async load() { + this.app.pluginSettingsManager.add('users-permissions.sync', { + title: tval('Synchronize', { ns: NAMESPACE }), + icon: 'SyncOutlined', + Component: UserDataSyncSource, + sort: 99, + aclSnippet: 'pm.user-data-sync', + }); + } +} + +export default PluginUserDataSyncClient; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/locale/index.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/locale/index.ts new file mode 100644 index 0000000000..2b94158816 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/locale/index.ts @@ -0,0 +1,16 @@ +/** + * 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 { useTranslation } from 'react-i18next'; + +export const NAMESPACE = 'user-data-sync'; + +export function useUserDataSyncSourceTranslation() { + return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' }); +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/schemas/user-data-sync-sources.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/schemas/user-data-sync-sources.ts new file mode 100644 index 0000000000..107d976c37 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/schemas/user-data-sync-sources.ts @@ -0,0 +1,523 @@ +/** + * 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 { ISchema } from '@nocobase/client'; + +export const sourceCollection = { + name: 'userDataSyncSources', + sortable: true, + filterTargetKey: 'id', + fields: [ + { + name: 'id', + type: 'string', + interface: 'id', + }, + { + interface: 'input', + type: 'string', + name: 'name', + uiSchema: { + type: 'string', + title: '{{t("Source name")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + interface: 'input', + type: 'string', + name: 'sourceType', + allowNull: false, + uiSchema: { + type: 'string', + title: '{{t("Type")}}', + 'x-component': 'Select', + required: true, + dataSource: '{{ types }}', + }, + }, + // { + // interface: 'input', + // type: 'string', + // name: 'displayName', + // uiSchema: { + // type: 'string', + // title: '{{t("Source display name")}}', + // 'x-component': 'Input', + // }, + // }, + { + type: 'boolean', + name: 'enabled', + uiSchema: { + type: 'boolean', + title: '{{t("Enabled")}}', + 'x-component': 'Checkbox', + }, + }, + ], +}; + +export const taskCollection = { + name: 'userDataSyncTasks', + filterTargetKey: 'id', + fields: [ + { + name: 'id', + type: 'bigInt', + interface: 'id', + }, + { + name: 'batch', + interface: 'input', + type: 'string', + allowNull: false, + uiSchema: { + type: 'string', + title: '{{t("Batch")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + name: 'source', + interface: 'input', + type: 'belongsTo', + target: 'userDataSyncSources', + targetKey: 'id', + foreignKey: 'sourceId', + allowNull: false, + uiSchema: { + type: 'object', + title: '{{t("Source")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'name', + }, + }, + required: true, + 'x-read-pretty': true, + }, + }, + { + name: 'status', + interface: 'input', + type: 'string', + allowNull: false, + uiSchema: { + type: 'string', + title: '{{t("Status")}}', + 'x-component': 'Select', + required: true, + enum: [ + { label: '{{t("Init")}}', value: 'init', color: 'default' }, + { label: '{{t("Processing")}}', value: 'processing', color: 'processing' }, + { label: '{{t("Success")}}', value: 'success', color: 'success' }, + { label: '{{t("Failed")}}', value: 'failed', color: 'error' }, + ], + }, + }, + { + name: 'message', + interface: 'input', + type: 'string', + allowNull: true, + uiSchema: { + type: 'string', + title: '{{t("Message")}}', + 'x-component': 'Input', + required: false, + }, + }, + { + name: 'cost', + interface: 'input', + type: 'integer', + allowNull: true, + uiSchema: { + type: 'integer', + title: '{{t("Cost")}}', + 'x-component': 'InputNumber', + 'x-component-props': { + precision: 0, + }, + required: false, + }, + }, + ], +}; + +export const createFormSchema: ISchema = { + type: 'object', + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + title: '{{t("Add new")}}', + properties: { + form: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCustomFormProps', + properties: { + name: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + sourceType: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-component-props': { + options: '{{ types }}', + }, + }, + // displayName: { + // 'x-component': 'CollectionField', + // 'x-decorator': 'FormItem', + // }, + enabled: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + options: { + type: 'object', + 'x-component': 'Options', + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-use-component-props': 'useSubmitActionProps', + 'x-component-props': { + type: 'primary', + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const tasksTableBlockSchema: ISchema = { + type: 'object', + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + title: '{{ t("Tasks") }}', + properties: { + table: { + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-use-decorator-props': 'useTasksTableBlockProps', + 'x-decorator-props': { + collection: taskCollection.name, + dragSort: false, + action: 'list', + showIndex: true, + }, + properties: { + table: { + type: 'array', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + }, + properties: { + batch: { + type: 'void', + title: '{{ t("Batch") }}', + 'x-component': 'TableV2.Column', + properties: { + batch: { + type: 'string', + 'x-component': 'CollectionField', + 'x-pattern': 'readPretty', + }, + }, + }, + status: { + type: 'void', + title: '{{ t("Status") }}', + 'x-component': 'TableV2.Column', + properties: { + status: { + type: 'string', + 'x-component': 'CollectionField', + 'x-pattern': 'readPretty', + }, + }, + }, + message: { + type: 'void', + title: '{{ t("Message") }}', + 'x-component': 'TableV2.Column', + properties: { + message: { + type: 'string', + 'x-component': 'CollectionField', + 'x-pattern': 'readPretty', + }, + }, + }, + actions: { + type: 'void', + title: '{{t("Actions")}}', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + sync: { + type: 'void', + title: '{{ t("Retry") }}', + 'x-component': 'Action.Link', + 'x-use-component-props': 'useRetryActionProps', + 'x-display': '{{ $record.status === "failed" ? "visible" : "hidden" }}', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +export const userDataSyncSourcesSchema: ISchema = { + type: 'void', + name: 'userDataSyncSources', + 'x-component': 'CardItem', + 'x-decorator': 'TableBlockProvider', + 'x-decorator-props': { + collection: sourceCollection.name, + dragSort: false, + action: 'list', + params: { + pageSize: 10, + }, + showIndex: true, + }, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 16, + }, + }, + properties: { + delete: { + type: 'void', + title: '{{t("Delete")}}', + 'x-component': 'Action', + 'x-use-component-props': 'useBulkDestroyActionProps', + 'x-component-props': { + icon: 'DeleteOutlined', + confirm: { + title: "{{t('Delete')}}", + content: "{{t('Are you sure you want to delete it?')}}", + }, + }, + }, + create: { + type: 'void', + title: '{{t("Add new")}}', + 'x-component': 'AddNew', + 'x-component-props': { + type: 'primary', + }, + }, + }, + }, + table: { + type: 'array', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + name: { + type: 'void', + title: '{{t("Source name")}}', + 'x-component': 'TableV2.Column', + properties: { + name: { + type: 'string', + 'x-component': 'CollectionField', + 'x-pattern': 'readPretty', + }, + }, + }, + // displayName: { + // type: 'void', + // title: '{{t("Source display name")}}', + // 'x-component': 'TableV2.Column', + // properties: { + // displayName: { + // type: 'string', + // 'x-component': 'CollectionField', + // 'x-pattern': 'readPretty', + // }, + // }, + // }, + sourceType: { + type: 'void', + title: '{{t("Type")}}', + 'x-component': 'TableV2.Column', + properties: { + sourceType: { + type: 'string', + 'x-component': 'Select', + 'x-pattern': 'readPretty', + enum: '{{ types }}', + }, + }, + }, + enabled: { + type: 'void', + title: '{{t("Enabled")}}', + 'x-component': 'TableV2.Column', + properties: { + enabled: { + type: 'boolean', + 'x-component': 'CollectionField', + 'x-pattern': 'readPretty', + }, + }, + }, + actions: { + type: 'void', + title: '{{t("Actions")}}', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + properties: { + actions: { + type: 'void', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + sync: { + type: 'void', + title: '{{ t("Sync") }}', + 'x-component': 'Action.Link', + 'x-use-component-props': 'useSyncActionProps', + 'x-display': '{{ $record.enabled ? "visible" : "hidden" }}', + }, + tasks: { + type: 'void', + title: '{{ t("Tasks") }}', + 'x-component': 'Tasks', + 'x-component-props': { + type: 'primary', + }, + 'x-display': '{{ $record.enabled ? "visible" : "hidden" }}', + }, + edit: { + type: 'void', + title: '{{t("Configure")}}', + 'x-component': 'Action.Link', + 'x-component-props': { + type: 'primary', + openMode: 'drawer', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'Action.Drawer', + title: '{{t("Configure")}}', + properties: { + form: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormProps', + properties: { + name: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + sourceType: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-component-props': { + options: '{{ types }}', + }, + }, + // displayName: { + // 'x-component': 'CollectionField', + // 'x-decorator': 'FormItem', + // }, + enabled: { + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + }, + options: { + type: 'object', + 'x-component': 'Options', + }, + footer: { + type: 'void', + 'x-component': 'Action.Drawer.Footer', + properties: { + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-use-component-props': 'useSubmitActionProps', + 'x-component-props': { + type: 'primary', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + delete: { + type: 'void', + title: '{{ t("Delete") }}', + 'x-component': 'Action.Link', + 'x-use-component-props': 'useDeleteActionProps', + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/client/sourceType.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/sourceType.ts new file mode 100644 index 0000000000..4c5845da78 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/client/sourceType.ts @@ -0,0 +1,27 @@ +/** + * 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 { createContext, useContext } from 'react'; + +export const SourceTypeContext = createContext<{ type: string }>({ type: '' }); +SourceTypeContext.displayName = 'SourceTypeContext'; + +export const SourceTypesContext = createContext<{ + types: { + key: string; + label: string; + value: string; + }[]; +}>({ types: [] }); +SourceTypesContext.displayName = 'SourceTypesContext'; + +export const useSourceTypes = () => { + const { types } = useContext(SourceTypesContext); + return types; +}; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/index.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/index.ts new file mode 100644 index 0000000000..53bc727360 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/index.ts @@ -0,0 +1,11 @@ +/** + * 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. + */ + +export * from './server'; +export { default, UserDataResource, FormatUser, SyncAccept, OriginRecord } from './server'; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/en-US.json new file mode 100644 index 0000000000..bd58e56b76 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/en-US.json @@ -0,0 +1,19 @@ +{ + "Synchronize": "Synchronize", + "Source name": "Source name", + "Source display name": "Source display name", + "Type": "Type", + "Sync": "Sync", + "Tasks": "Tasks", + "Batch": "Batch", + "Status": "Status", + "Message": "Message", + "Init": "Init", + "Processing": "Processing", + "Success": "Success", + "Failed": "Failed", + "Authenticator": "Authenticator", + "Retry": "Retry", + "No user data source plugin installed": "No user data source plugin installed", + "View documentation": "View documentation" +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/zh-CN.json new file mode 100644 index 0000000000..dc72473030 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/locale/zh-CN.json @@ -0,0 +1,22 @@ +{ + "Synchronize": "同步", + "Source name": "数据源名称", + "Source display name": "数据源展示名称", + "Type": "类型", + "Sync": "同步", + "Tasks": "任务", + "Batch": "批次", + "Status": "状态", + "Message": "信息", + "Init": "初始化", + "Processing": "进行中", + "Success": "成功", + "Failed": "失败", + "Authenticator": "用户认证", + "dingtalk": "钉钉", + "wecom": "企业微信", + "default": "默认", + "Retry": "重试", + "No user data source plugin installed": "未安装同步数据源", + "View documentation": "查看文档" +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts new file mode 100644 index 0000000000..dd066ead86 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/api.test.ts @@ -0,0 +1,54 @@ +/** + * 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 { MockServer, createMockServer } from '@nocobase/test'; +import { UserDataResourceManager } from '../user-data-resource-manager'; +import { MockUsersResource } from './mock-resource'; +import PluginUserDataSyncServer from '../plugin'; + +describe('api', async () => { + let app: MockServer; + let agent: any; + let resourceManager: UserDataResourceManager; + + beforeEach(async () => { + app = await createMockServer({ + plugins: ['user-data-sync'], + }); + agent = app.agent(); + const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer; + resourceManager = plugin.resourceManager; + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('push data', async () => { + const usersResource = new MockUsersResource(app.db, app.logger); + resourceManager.registerResource(usersResource); + const res = await agent.resource('userData').push({ + values: { + dataType: 'user', + records: [ + { + uid: '1', + nickname: 'test', + }, + ], + }, + }); + expect(res.status).toBe(200); + expect(usersResource.data.length).toBe(1); + expect(usersResource.data[0]).toMatchObject({ + uid: '1', + nickname: 'test', + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/mock-resource.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/mock-resource.ts new file mode 100644 index 0000000000..caf964996a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/mock-resource.ts @@ -0,0 +1,41 @@ +/** + * 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 { + OriginRecord, + PrimaryKey, + RecordResourceChanged, + SyncAccept, + UserDataResource, +} from '../user-data-resource-manager'; + +export class MockUsersResource extends UserDataResource { + name = 'mock-users'; + accepts: SyncAccept[] = ['user']; + data = []; + + async update(record: OriginRecord, resourcePks: PrimaryKey[]): Promise { + this.data[resourcePks[0]] = record.metaData; + return []; + } + + async create(record: OriginRecord, matchKey: string): Promise { + this.data.push(record.metaData); + return [{ resourcesPk: this.data.length - 1, isDeleted: false }]; + } +} + +export class ErrorResource extends UserDataResource { + async update(record: OriginRecord, resourcePks: PrimaryKey[]): Promise { + return []; + } + async create(record: OriginRecord, matchKey: string): Promise { + return []; + } +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts new file mode 100644 index 0000000000..8319865b93 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/__tests__/resource-manager.test.ts @@ -0,0 +1,153 @@ +/** + * 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 { MockDatabase, MockServer, createMockServer } from '@nocobase/test'; +import { UserDataResourceManager } from '../user-data-resource-manager'; +import { ErrorResource, MockUsersResource } from './mock-resource'; + +describe('user-data-resource-manager', () => { + let app: MockServer; + let db: MockDatabase; + let resourceManager: UserDataResourceManager; + + beforeEach(async () => { + app = await createMockServer({ + plugins: ['user-data-sync'], + }); + db = app.db; + resourceManager = new UserDataResourceManager(); + resourceManager.db = db; + }); + + afterEach(async () => { + await db.clean({ drop: true }); + await app.destroy(); + }); + + it('register resource error', async () => { + try { + const errResource = new ErrorResource(db, app.logger); + expect(resourceManager.registerResource(errResource)).toThrowError( + '"name" for user data synchronize resource is required', + ); + const errResource2 = new ErrorResource(db, app.logger); + errResource2.name = 'error'; + expect(resourceManager.registerResource(errResource2)).toThrowError( + '"accepts" for user data synchronize resource is required', + ); + } catch (error) { + // ... + } + }); + + it('register resource in order', async () => { + const usersResource = new MockUsersResource(db, app.logger); + resourceManager.registerResource(usersResource, { after: 'mock-users2' }); + const usersResource2 = new MockUsersResource(db, app.logger); + usersResource2.name = 'mock-users2'; + resourceManager.registerResource(usersResource2); + const nodes = resourceManager.resources.nodes; + expect(nodes.length).toBe(2); + expect(nodes).toEqual([usersResource2, usersResource]); + }); + + it('create for a resource', async () => { + const mockUsersResource = new MockUsersResource(db, app.logger); + resourceManager.registerResource(mockUsersResource); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + records: [ + { + uid: '1', + nickname: 'test', + }, + ], + matchKey: 'uid', + }); + expect(mockUsersResource.data.length).toBe(1); + expect(mockUsersResource.data[0]).toMatchObject({ + uid: '1', + nickname: 'test', + }); + const originRecords = await resourceManager.findOriginRecords({ + sourceName: 'test', + dataType: 'user', + sourceUks: ['1'], + }); + expect(originRecords.length).toBe(1); + expect(originRecords[0]).toMatchObject({ + sourceName: 'test', + dataType: 'user', + sourceUk: '1', + metaData: { + uid: '1', + nickname: 'test', + }, + resources: [ + { + resource: 'mock-users', + resourcePk: '0', + }, + ], + }); + }); + + it('update for a resource', async () => { + const mockUsersResource = new MockUsersResource(db, app.logger); + resourceManager.registerResource(mockUsersResource); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + records: [ + { + uid: '1', + nickname: 'test', + }, + ], + }); + expect(mockUsersResource.data.length).toBe(1); + expect(mockUsersResource.data[0]).toMatchObject({ + nickname: 'test', + }); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + records: [ + { + uid: '1', + nickname: 'test2', + }, + ], + }); + expect(mockUsersResource.data.length).toBe(1); + expect(mockUsersResource.data[0]).toMatchObject({ + nickname: 'test2', + }); + const originRecords = await resourceManager.findOriginRecords({ + sourceName: 'test', + dataType: 'user', + sourceUks: ['1'], + }); + expect(originRecords.length).toBe(1); + expect(originRecords[0]).toMatchObject({ + sourceName: 'test', + dataType: 'user', + sourceUk: '1', + metaData: { + uid: '1', + nickname: 'test2', + }, + lastMetaData: { + uid: '1', + nickname: 'test', + }, + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/actions/user-data.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/actions/user-data.ts new file mode 100644 index 0000000000..6aa15f9326 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/actions/user-data.ts @@ -0,0 +1,44 @@ +/** + * 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 { Context, Next } from '@nocobase/actions'; +import { PluginUserDataSyncServer } from '../plugin'; + +export default { + listSyncTypes: async (ctx: Context, next: Next) => { + const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer; + ctx.body = plugin.sourceManager.listTypes(); + await next(); + }, + pull: async (ctx: Context, next: Next) => { + const { name } = ctx.action.params; + const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer; + await plugin.syncService.pull(name, ctx); + await next(); + }, + push: async (ctx: Context, next: Next) => { + const data = ctx.action.params.values || {}; + const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer; + try { + const result = await plugin.syncService.push(data); + ctx.body = { code: 0, message: 'success', result }; + } catch (error) { + ctx.status = 500; + ctx.body = { code: 500, message: error.message }; + return; + } + await next(); + }, + retry: async (ctx: Context, next: Next) => { + const { sourceId, id } = ctx.action.params; + const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer; + await plugin.syncService.retry(sourceId, id, ctx); + await next(); + }, +}; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/.gitkeep b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records-resources.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records-resources.ts new file mode 100644 index 0000000000..b104bebd58 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records-resources.ts @@ -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 { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + name: 'userDataSyncRecordsResources', + fields: [ + { + name: 'recordId', + type: 'bigInt', + interface: 'id', + }, + { + name: 'resource', + interface: 'Select', + type: 'string', + allowNull: false, + }, + { + name: 'resourcePk', + interface: 'Input', + type: 'string', + allowNull: true, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records.ts new file mode 100644 index 0000000000..54e5c90bb8 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-records.ts @@ -0,0 +1,64 @@ +/** + * 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 { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + dumpRules: { + group: 'third-party', + }, + shared: true, + name: 'userDataSyncRecords', + createdAt: true, + updatedAt: true, + logging: true, + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + interface: 'id', + }, + { + name: 'sourceName', + interface: 'Input', + type: 'string', + allowNull: false, + }, + { + name: 'sourceUk', + interface: 'Input', + type: 'string', + allowNull: false, + }, + { + name: 'dataType', + interface: 'Select', + type: 'string', + allowNull: false, + }, + { + name: 'resources', + type: 'hasMany', + target: 'userDataSyncRecordsResources', + sourceKey: 'id', + foreignKey: 'recordId', + }, + { + type: 'json', + name: 'metaData', + }, + { + type: 'json', + name: 'lastMetaData', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-sources.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-sources.ts new file mode 100644 index 0000000000..cedaaceadb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-sources.ts @@ -0,0 +1,88 @@ +/** + * 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 { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + dumpRules: { + group: 'third-party', + }, + shared: true, + name: 'userDataSyncSources', + title: '{{t("Sync Sources")}}', + sortable: true, + model: 'SyncSourceModel', + createdBy: true, + updatedBy: true, + logging: true, + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + interface: 'id', + }, + { + interface: 'input', + type: 'string', + name: 'name', + allowNull: false, + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Source name")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + interface: 'input', + type: 'string', + name: 'sourceType', + allowNull: false, + uiSchema: { + type: 'string', + title: '{{t("Source Type")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + interface: 'input', + type: 'string', + name: 'displayName', + uiSchema: { + type: 'string', + title: '{{t("Source display name")}}', + 'x-component': 'Input', + }, + translation: true, + }, + { + type: 'boolean', + name: 'enabled', + defaultValue: false, + }, + { + type: 'json', + name: 'options', + allowNull: false, + defaultValue: {}, + }, + { + type: 'hasMany', + name: 'tasks', + target: 'userDataSyncTasks', + sourceKey: 'id', + foreignKey: 'sourceId', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-tasks.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-tasks.ts new file mode 100644 index 0000000000..95d4d5de5f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/collections/user-data-sync-tasks.ts @@ -0,0 +1,110 @@ +/** + * 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 { defineCollection } from '@nocobase/database'; + +export default defineCollection({ + dumpRules: { + group: 'third-party', + }, + name: 'userDataSyncTasks', + title: '{{t("Sync Tasks")}}', + sortable: 'sort', + model: 'SyncTaskModel', + createdBy: true, + updatedBy: true, + createdAt: true, + updatedAt: true, + logging: true, + shared: true, + fields: [ + { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + interface: 'id', + }, + { + name: 'batch', + interface: 'input', + type: 'string', + allowNull: false, + unique: true, + uiSchema: { + type: 'string', + title: '{{t("Batch")}}', + 'x-component': 'Input', + required: true, + }, + }, + { + name: 'source', + interface: 'input', + type: 'belongsTo', + target: 'userDataSyncSources', + targetKey: 'id', + foreignKey: 'sourceId', + allowNull: false, + uiSchema: { + type: 'object', + title: '{{t("Source")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'name', + }, + }, + required: true, + 'x-read-pretty': true, + }, + }, + { + name: 'status', + interface: 'Select', + type: 'string', + allowNull: false, + uiSchema: { + type: 'string', + title: '{{t("Status")}}', + 'x-component': 'Select', + required: true, + }, + }, + { + name: 'message', + interface: 'input', + type: 'string', + allowNull: true, + uiSchema: { + type: 'string', + title: '{{t("Message")}}', + 'x-component': 'Input', + required: false, + }, + }, + { + name: 'cost', + interface: 'input', + type: 'integer', + allowNull: true, + uiSchema: { + type: 'integer', + title: '{{t("Cost")}}', + 'x-component': 'InputNumber', + 'x-component-props': { + precision: 0, + }, + required: false, + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/index.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/index.ts new file mode 100644 index 0000000000..0384832b85 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/index.ts @@ -0,0 +1,12 @@ +/** + * 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. + */ + +export { SyncSource } from './sync-source'; +export * from './user-data-resource-manager'; +export { default } from './plugin'; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/models/sync-source.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/models/sync-source.ts new file mode 100644 index 0000000000..647d28570d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/models/sync-source.ts @@ -0,0 +1,17 @@ +/** + * 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 { Model } from '@nocobase/database'; + +export class SyncSourceModel extends Model { + declare id: number; + declare name: string; + declare sourceType: string; + declare options: any; +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin.ts new file mode 100644 index 0000000000..1bc33301c4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin.ts @@ -0,0 +1,71 @@ +/** + * 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 { Plugin } from '@nocobase/server'; +import { UserDataResourceManager } from './user-data-resource-manager'; +import { UserDataSyncService } from './user-data-sync-service'; +import userDataActions from './actions/user-data'; +import { SyncSourceManager } from './sync-source-manager'; +import { SyncSourceModel } from './models/sync-source'; +import { LoggerOptions, Logger } from '@nocobase/logger'; + +export class PluginUserDataSyncServer extends Plugin { + sourceManager: SyncSourceManager; + resourceManager: UserDataResourceManager; + syncService: UserDataSyncService; + + async afterAdd() {} + + async beforeLoad() { + this.app.db.registerModels({ SyncSourceModel }); + this.sourceManager = new SyncSourceManager(); + this.resourceManager = new UserDataResourceManager(); + } + + getLogger(): Logger { + const logger = this.createLogger({ + dirname: 'user-data-sync', + filename: '%DATE%.log', + format: 'json', + } as LoggerOptions); + + return logger; + } + + async load() { + const logger = this.getLogger(); + this.resourceManager.db = this.app.db; + this.resourceManager.logger = this.app.logger; + this.syncService = new UserDataSyncService(this.resourceManager, this.sourceManager, logger); + this.app.resourceManager.define({ + name: 'userData', + actions: { + listSyncTypes: userDataActions.listSyncTypes, + pull: userDataActions.pull, + push: userDataActions.push, + retry: userDataActions.retry, + }, + }); + + this.app.acl.registerSnippet({ + name: `pm.${this.name}`, + actions: ['userData:*', 'userDataSyncSources:*'], + }); + } + + async install() {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginUserDataSyncServer; diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source-manager.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source-manager.ts new file mode 100644 index 0000000000..c27525cfb9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source-manager.ts @@ -0,0 +1,58 @@ +/** + * 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 { Registry } from '@nocobase/utils'; +import { SyncSource, SyncSourceExtend } from './sync-source'; +import { Context } from '@nocobase/actions'; +import { SyncSourceModel } from './models/sync-source'; + +type SyncSourceConfig = { + syncSource: SyncSourceExtend; + title?: string; +}; + +export class SyncSourceManager { + protected syncSourceTypes: Registry = new Registry(); + registerType(syncSourceType: string, syncSourceConfig: SyncSourceConfig) { + this.syncSourceTypes.register(syncSourceType, syncSourceConfig); + } + + listTypes() { + return Array.from(this.syncSourceTypes.getEntities()).map(([syncSourceType, source]) => ({ + name: syncSourceType, + title: source.title, + })); + } + + async getByName(name: string, ctx: Context) { + const repo = ctx.db.getRepository('userDataSyncSources'); + const sourceInstance: SyncSourceModel = await repo.findOne({ filter: { enabled: true, name: name } }); + if (!sourceInstance) { + throw new Error(`SyncSource [${name}] is not found.`); + } + return this.create(sourceInstance, ctx); + } + + async getById(id: number, ctx: Context) { + const repo = ctx.db.getRepository('userDataSyncSources'); + const sourceInstance: SyncSourceModel = await repo.findOne({ filter: { enabled: true }, filterByTk: id }); + if (!sourceInstance) { + throw new Error(`SyncSource [${id}] is not found.`); + } + return this.create(sourceInstance, ctx); + } + + create(sourceInstance: SyncSourceModel, ctx: Context) { + const { syncSource } = this.syncSourceTypes.get(sourceInstance.sourceType) || {}; + if (!syncSource) { + throw new Error(`SyncSourceType [${sourceInstance.sourceType}] is not found.`); + } + return new syncSource({ sourceInstance: sourceInstance, options: sourceInstance.options, ctx }); + } +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source.ts new file mode 100644 index 0000000000..0be03bb65a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/sync-source.ts @@ -0,0 +1,106 @@ +/** + * 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 { Context } from '@nocobase/actions'; +import { SyncSourceModel } from './models/sync-source'; +import { UserData } from './user-data-resource-manager'; +import dayjs from 'dayjs'; + +export type SyncSourceConfig = { + sourceInstance: SyncSourceModel; + options: { + [key: string]: any; + }; + ctx: Context; +}; + +interface ISyncSource { + pull(): Promise; +} + +export abstract class SyncSource implements ISyncSource { + instance: SyncSourceModel; + protected options: { + [key: string]: any; + }; + protected ctx: Context; + + constructor(config: SyncSourceConfig) { + const { options, ctx, sourceInstance } = config; + this.instance = sourceInstance; + this.options = options; + this.ctx = ctx; + } + + abstract pull(): Promise; + + async newTask() { + const batch = generateUniqueNumber(); + return await this.instance.createTask({ batch, status: 'init' }); + } + + async beginTask(taskId: number) { + const tasks = await this.instance.getTasks({ where: { id: taskId } }); + if (!tasks && !tasks.length) { + throw new Error(`Task [${taskId}] is not found.`); + } + const task = tasks[0]; + if (task.status !== 'init') { + throw new Error(`Task [${taskId}] is not init.`); + } + task.status = 'processing'; + await task.save(); + } + + async endTask(params: EndTaskParams) { + const { taskId, success, cost, message } = params; + const tasks = await this.instance.getTasks({ where: { id: taskId } }); + if (!tasks && !tasks.length) { + throw new Error(`Task [${taskId}] is not found.`); + } + const task = tasks[0]; + if (task.status !== 'processing') { + throw new Error(`Task [${taskId}] is not processing.`); + } + task.status = success ? 'success' : 'failed'; + task.cost = cost; + task.message = message; + await task.save(); + } + + async retryTask(taskId: number) { + const tasks = await this.instance.getTasks({ where: { id: taskId } }); + if (!tasks && !tasks.length) { + throw new Error(`Task [${taskId}] is not found.`); + } + const task = tasks[0]; + if (task.status !== 'failed') { + throw new Error(`Task [${taskId}] is not failed.`); + } + task.status = 'processing'; + task.message = ''; + await task.save(); + return task; + } +} + +export type SyncSourceExtend = new (config: SyncSourceConfig) => T; + +type EndTaskParams = { + taskId: number; + success: boolean; + cost?: number; + message?: string; +}; + +function generateUniqueNumber() { + const formattedDate = dayjs().format('YYYYMMDDHHmmss'); + const randomDigits = Math.floor(100000 + Math.random() * 900000); + return formattedDate + randomDigits; +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts new file mode 100644 index 0000000000..fa0aac086e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts @@ -0,0 +1,268 @@ +/** + * 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 { Toposort, ToposortOptions } from '@nocobase/utils'; +import Database, { Repository } from '@nocobase/database'; +import { SystemLogger } from '@nocobase/logger'; + +export type FormatUser = { + uid: string; + username?: string; + email?: string; + nickname?: string; + phone?: string; + departments?: string[]; + isDeleted?: boolean; + [key: string]: any; +}; + +export type FormatDepartment = { + uid: string; + title?: string; + parentUid?: string; + isDeleted?: boolean; + [key: string]: any; +}; + +export type UserDataRecord = FormatUser | FormatDepartment; + +export type SyncDataType = 'user' | 'department'; + +export type SyncAccept = SyncDataType; + +export type OriginRecord = { + id: number; + sourceName: string; + sourceUk: string; + dataType: SyncDataType; + metaData: UserDataRecord; + resources: { + resource: string; + resourcePk: string; + }[]; +}; + +export type UserData = { + dataType: SyncDataType; + matchKey?: string; + records: UserDataRecord[]; + sourceName: string; +}; + +export type PrimaryKey = number | string; + +export type RecordResourceChanged = { + resourcesPk: PrimaryKey; + isDeleted: boolean; +}; + +export abstract class UserDataResource { + name: string; + accepts: SyncAccept[]; + db: Database; + logger: SystemLogger; + + constructor(db: Database, logger: SystemLogger) { + this.db = db; + this.logger = logger; + } + + abstract update(record: OriginRecord, resourcePks: PrimaryKey[], matchKey?: string): Promise; + abstract create(record: OriginRecord, matchKey: string): Promise; + + get syncRecordRepo() { + return this.db.getRepository('userDataSyncRecords'); + } + + get syncRecordResourceRepo() { + return this.db.getRepository('userDataSyncRecordsResources'); + } +} + +export type SyncResult = { + resource: string; + detail: { + count: { + all: number; + success: number; + failed: number; + }; + failedRecords: { + record: UserDataRecord; + message: string; + }[]; + }; +}; + +export class UserDataResourceManager { + resources = new Toposort(); + syncRecordRepo: Repository; + syncRecordResourceRepo: Repository; + logger: SystemLogger; + + registerResource(resource: UserDataResource, options?: ToposortOptions) { + if (!resource.name) { + throw new Error('"name" for user data synchronize resource is required'); + } + if (!resource.accepts) { + throw new Error('"accepts" for user data synchronize resource is required'); + } + this.resources.add(resource, { tag: resource.name, ...options }); + } + + set db(value: Database) { + this.syncRecordRepo = value.getRepository('userDataSyncRecords'); + this.syncRecordResourceRepo = value.getRepository('userDataSyncRecordsResources'); + } + + async saveOriginRecords(data: UserData): Promise { + for (const record of data.records) { + if (record.uid === undefined) { + throw new Error(`record must has uid, error record: ${JSON.stringify(record)}`); + } + const syncRecord = await this.syncRecordRepo.findOne({ + where: { + sourceName: data.sourceName, + sourceUk: record.uid, + dataType: data.dataType, + }, + }); + if (syncRecord) { + syncRecord.lastMetaData = syncRecord.metaData; + syncRecord.metaData = record; + await syncRecord.save(); + } else { + await this.syncRecordRepo.create({ + values: { + sourceName: data.sourceName, + sourceUk: record.uid, + dataType: data.dataType, + metaData: record, + }, + }); + } + } + } + + async findOriginRecords({ sourceName, dataType, sourceUks }): Promise { + return await this.syncRecordRepo.find({ + appends: ['resources'], + filter: { sourceName, dataType, sourceUk: { $in: sourceUks } }, + }); + } + + async addResourceToOriginRecord({ recordId, resource, resourcePk }): Promise { + const syncRecord = await this.syncRecordRepo.findOne({ + filter: { + id: recordId, + }, + }); + if (syncRecord) { + await syncRecord.createResource({ + resource, + resourcePk, + }); + } + } + + async removeResourceFromOriginRecord({ recordId, resource, resourcePk }): Promise { + const recordResource = await this.syncRecordResourceRepo.findOne({ + where: { + recordId, + resource, + resourcePk, + }, + }); + if (recordResource) { + await recordResource.destroy(); + } + } + + async updateOrCreate(data: UserData): Promise { + await this.saveOriginRecords(data); + const { dataType, sourceName, records, matchKey } = data; + const sourceUks = records.map((record) => record.uid); + let processed = false; + const syncResults: SyncResult[] = []; + for (const resource of this.resources.nodes) { + if (!resource.accepts.includes(dataType)) { + continue; + } + const associateResource = resource.name; + processed = true; + const originRecords = await this.findOriginRecords({ sourceName, sourceUks, dataType }); + if (!(originRecords && originRecords.length)) { + continue; + } + const successRecords = []; + const failedRecords = []; + for (const originRecord of originRecords) { + const resourceRecords = originRecord.resources?.filter( + (r: { resource: string }) => r.resource === associateResource, + ); + let recordResourceChangeds: RecordResourceChanged[]; + if (resourceRecords && resourceRecords.length > 0) { + const resourcePks = resourceRecords.map((r: { resourcePk: string }) => r.resourcePk); + try { + recordResourceChangeds = await resource.update(originRecord, resourcePks, matchKey); + this.logger?.debug(`update record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`); + successRecords.push(originRecord.metaData); + } catch (error) { + this.logger?.warn(`update record error: ${error.message}`, { originRecord }); + failedRecords.push({ record: originRecord.metaData, message: error.message }); + continue; + } + } else { + try { + recordResourceChangeds = await resource.create(originRecord, matchKey); + this.logger?.debug(`create record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`); + successRecords.push(originRecord.metaData); + } catch (error) { + this.logger?.warn(`create record error: ${error.message}`, { originRecord }); + failedRecords.push({ record: originRecord.metaData, message: error.message }); + continue; + } + } + if (!recordResourceChangeds || recordResourceChangeds.length === 0) { + continue; + } + for (const { resourcesPk, isDeleted } of recordResourceChangeds) { + if (isDeleted) { + await this.removeResourceFromOriginRecord({ + recordId: originRecord.id, + resource: associateResource, + resourcePk: resourcesPk, + }); + } else { + await this.addResourceToOriginRecord({ + recordId: originRecord.id, + resource: associateResource, + resourcePk: resourcesPk, + }); + } + } + } + syncResults.push({ + resource: associateResource, + detail: { + count: { + all: originRecords.length, + success: successRecords.length, + failed: failedRecords.length, + }, + failedRecords, + }, + }); + } + if (!processed) { + throw new Error(`dataType "${dataType}" is not support`); + } + return syncResults; + } +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-sync-service.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-sync-service.ts new file mode 100644 index 0000000000..76bd25f69b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-sync-service.ts @@ -0,0 +1,111 @@ +/** + * 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 { SyncResult, UserData, UserDataResourceManager } from './user-data-resource-manager'; +import { SyncSourceManager } from './sync-source-manager'; +import { Context } from '@nocobase/actions'; +import { SyncSource } from './sync-source'; +import { Logger } from '@nocobase/logger'; + +export class UserDataSyncService { + resourceManager: UserDataResourceManager; + sourceManager: SyncSourceManager; + logger: Logger; + + constructor(resourceManager: UserDataResourceManager, sourceManager: SyncSourceManager, logger: Logger) { + this.resourceManager = resourceManager; + this.sourceManager = sourceManager; + this.logger = logger; + } + + async pull(sourceName: string, ctx: Context) { + const source = await this.sourceManager.getByName(sourceName, ctx); + const task = await source.newTask(); + await source.beginTask(task.id); + ctx.log.info('begin sync task of source', { source: sourceName, sourceType: source.instance.sourceType }); + this.runSync(source, task, ctx); + } + + async push(data: any): Promise { + const { dataType, records } = data; + if (dataType === undefined) { + throw new Error('dataType for user data synchronize is required'); + } + if (dataType !== 'user' && dataType !== 'department') { + throw new Error('dataType must be user or department'); + } + if (records === undefined) { + throw new Error('records for user data synchronize is required'); + } + if (records.length === 0) { + throw new Error('records must have at least one piece of data'); + } + const userData: UserData = { + dataType: data.dataType, + matchKey: data.matchKey, + records: data.records, + sourceName: data.sourceName ? data.sourceName : 'api', + }; + this.logger.info({ + source: data.sourceName ? data.sourceName : 'api', + sourceType: 'api', + data: data, + }); + return await this.resourceManager.updateOrCreate(userData); + } + + async retry(sourceId: number, taskId: number, ctx: Context) { + const source = await this.sourceManager.getById(sourceId, ctx); + const task = await source.retryTask(taskId); + ctx.log.info('retry sync task of source', { + source: source.instance.name, + sourceType: source.instance.name, + task: task.id, + }); + this.runSync(source, task, ctx); + } + + async runSync(source: SyncSource, task: any, ctx: Context) { + const currentTimeMillis = new Date().getTime(); + try { + ctx.log.info('begin pull data of source', { + source: source.instance.name, + sourceType: source.instance.sourceType, + }); + const data = await source.pull(); + // 输出拉取的数据 + this.logger.info({ + source: source.instance.name, + sourceType: source.instance.sourceType, + batch: task.batch, + data: data, + }); + ctx.log.info('end pull data of source', { source: source.instance.name, sourceType: source.instance.sourceType }); + ctx.log.info('begin update data of source', { + source: source.instance.name, + sourceType: source.instance.sourceType, + }); + for (const item of data) { + await this.resourceManager.updateOrCreate(item); + } + ctx.log.info('end update data of source', { + source: source.instance.name, + sourceType: source.instance.sourceType, + }); + const costTime = new Date().getTime() - currentTimeMillis; + await source.endTask({ taskId: task.id, success: true, cost: costTime }); + } catch (err) { + ctx.log.error( + `sync task of source: ${source.instance.name} sourceType: ${source.instance.sourceType} error: ${err.message}`, + { method: 'runSync', err: err.stack, cause: err.cause }, + ); + await source.endTask({ taskId: task.id, success: false, message: err.message }); + } + } +} diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/swagger/index.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/swagger/index.ts new file mode 100644 index 0000000000..6cb7f81a0d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/swagger/index.ts @@ -0,0 +1,121 @@ +/** + * 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. + */ + +export default { + info: { + title: 'NocoBase API - User data synchronization plugin', + }, + paths: { + '/userData:push': { + post: { + description: 'Push user data', + tags: ['Push'], + security: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/userData', + }, + }, + }, + }, + }, + responses: { + 200: { + description: 'ok', + }, + }, + }, + }, + }, + components: { + schemas: { + userData: { + type: 'object', + description: '用户数据', + properties: { + dataType: { + type: 'string', + description: '数据类型, 目前可选值为: user, department', + }, + uniqueKey: { + type: 'string', + description: '唯一键', + }, + records: { + type: 'array', + description: + '数据, 若 dataType 为 user, 则为用户数据字段见schemas/user, 若 dataType 为 department, 则为部门数据字段见schemas/department', + items: { + type: 'object', + }, + }, + sourceName: { + type: 'string', + description: '数据源名称', + }, + }, + }, + user: { + type: 'object', + description: '用户', + properties: { + id: { + type: 'integer', + description: 'ID', + }, + nickname: { + type: 'string', + description: '昵称', + }, + email: { + type: 'string', + description: '邮箱', + }, + phone: { + type: 'string', + description: '手机号', + }, + departments: { + type: 'array', + description: '所属部门, 部门ID 数组', + items: { + type: 'string', + }, + }, + }, + }, + department: { + type: 'object', + description: '部门', + properties: { + id: { + type: 'string', + description: 'ID', + }, + name: { + type: 'string', + description: '名称', + }, + parentId: { + type: 'string', + description: '父级部门ID', + }, + }, + }, + }, + }, +}; + +/* +/api/userData:push +*/ diff --git a/packages/plugins/@nocobase/plugin-users/package.json b/packages/plugins/@nocobase/plugin-users/package.json index de694f975f..d1163f130a 100644 --- a/packages/plugins/@nocobase/plugin-users/package.json +++ b/packages/plugins/@nocobase/plugin-users/package.json @@ -19,6 +19,7 @@ "@nocobase/database": "1.x", "@nocobase/plugin-acl": "1.x", "@nocobase/plugin-auth": "1.x", + "@nocobase/plugin-user-data-sync": "1.x", "@nocobase/resourcer": "1.x", "@nocobase/server": "1.x", "@nocobase/test": "1.x", diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts new file mode 100644 index 0000000000..816925a7dd --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts @@ -0,0 +1,122 @@ +/** + * 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 { UserDataResourceManager } from '@nocobase/plugin-user-data-sync'; +import { MockDatabase, MockServer, createMockServer } from '@nocobase/test'; +import PluginUserDataSyncServer from 'packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin'; + +describe('user data sync', () => { + let app: MockServer; + let db: MockDatabase; + let resourceManager: UserDataResourceManager; + + beforeEach(async () => { + app = await createMockServer({ + plugins: ['user-data-sync', 'users'], + }); + db = app.db; + const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer; + resourceManager = plugin.resourceManager; + }); + + afterEach(async () => { + await db.clean({ drop: true }); + await app.destroy(); + }); + + it('should create user', async () => { + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test', + email: 'test@nocobase.com', + }, + ], + }); + const user = await db.getRepository('users').findOne({ + filter: { + email: 'test@nocobase.com', + }, + }); + expect(user).toBeTruthy(); + expect(user.nickname).toBe('test'); + }); + + it('should update existing user when creating', async () => { + const user = await db.getRepository('users').create({ + values: { + email: 'test@nocobase.com', + }, + }); + expect(user.nickname).toBeFalsy(); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test', + email: 'test@nocobase.com', + }, + ], + }); + const user2 = await db.getRepository('users').findOne({ + filter: { + id: user.id, + }, + }); + expect(user2).toBeTruthy(); + expect(user2.nickname).toBe('test'); + }); + + it('shoud update user', async () => { + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test', + email: 'test@nocobase.com', + }, + ], + }); + const user = await db.getRepository('users').findOne({ + filter: { + email: 'test@nocobase.com', + }, + }); + expect(user).toBeTruthy(); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test2', + email: 'test@nocobase.com', + }, + ], + }); + const user2 = await db.getRepository('users').findOne({ + filter: { + id: user.id, + }, + }); + expect(user2).toBeTruthy(); + expect(user2.nickname).toBe('test2'); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index 54aa0f0c44..ac4d218e0b 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -11,10 +11,11 @@ import { Collection, Op } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; import { parse } from '@nocobase/utils'; import { resolve } from 'path'; - import { Cache } from '@nocobase/cache'; import * as actions from './actions/users'; import { UserModel } from './models/UserModel'; +import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync'; +import { UserDataSyncResource } from './user-data-sync-resource'; export default class PluginUsersServer extends Plugin { async beforeLoad() { @@ -179,6 +180,11 @@ export default class PluginUsersServer extends Plugin { } } }); + + const userDataSyncPlugin = this.app.pm.get('user-data-sync') as PluginUserDataSyncServer; + if (userDataSyncPlugin) { + userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger)); + } } getInstallingData(options: any = {}) { diff --git a/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts b/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts new file mode 100644 index 0000000000..ec9137059b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts @@ -0,0 +1,102 @@ +/** + * 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 { Model } from '@nocobase/database'; +import { + FormatUser, + OriginRecord, + PrimaryKey, + RecordResourceChanged, + SyncAccept, + UserDataResource, +} from '@nocobase/plugin-user-data-sync'; + +export class UserDataSyncResource extends UserDataResource { + name = 'users'; + accepts: SyncAccept[] = ['user']; + + get userRepo() { + return this.db.getRepository('users'); + } + + async updateUser(user: Model, sourceUser: FormatUser) { + if (sourceUser.isDeleted) { + // 删除用户 + const roles = await user.getRoles(); + // 是否有Root角色 + for (const role of roles) { + if (role.name === 'root') { + return; + } + } + await user.destroy(); + return; + } + let dataChanged = false; + if (sourceUser.username !== undefined && user.username !== sourceUser.username) { + user.username = sourceUser.username; + dataChanged = true; + } + if (sourceUser.phone !== undefined && user.phone !== sourceUser.phone) { + user.phone = sourceUser.phone; + dataChanged = true; + } + if (sourceUser.email !== undefined && user.email !== sourceUser.email) { + user.email = sourceUser.email; + dataChanged = true; + } + if (sourceUser.nickname !== undefined && user.nickname !== sourceUser.nickname) { + user.nickname = sourceUser.nickname; + dataChanged = true; + } + if (dataChanged) { + await user.save(); + } + } + + async update(record: OriginRecord, resourcePks: PrimaryKey[], matchKey: string): Promise { + const { metaData: sourceUser } = record; + const resourcePk = resourcePks[0]; + const user = await this.userRepo.findOne({ + filterByTk: resourcePk, + }); + if (!user) { + // 用户不存在, 重新创建用户 + const result = await this.create(record, matchKey); + return [...result, { resourcesPk: resourcePk, isDeleted: true }]; + } + await this.updateUser(user, sourceUser); + return []; + } + + async create(record: OriginRecord, matchKey: string): Promise { + const { metaData: sourceUser } = record; + const filter = {}; + let user: any; + if (['phone', 'email', 'username'].includes(matchKey)) { + filter[matchKey] = sourceUser[matchKey]; + user = await this.userRepo.findOne({ + filter, + }); + } + if (user) { + await this.updateUser(user, sourceUser); + } else { + user = await this.userRepo.create({ + values: { + nickname: sourceUser.nickname, + phone: sourceUser.phone, + email: sourceUser.email, + username: sourceUser.username, + }, + }); + } + return [{ resourcesPk: user.id, isDeleted: false }]; + } +} diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 5b89f1b465..abc22716df 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -52,6 +52,7 @@ "@nocobase/plugin-theme-editor": "1.4.0-alpha", "@nocobase/plugin-ui-schema-storage": "1.4.0-alpha", "@nocobase/plugin-users": "1.4.0-alpha", + "@nocobase/plugin-user-data-sync": "1.4.0-alpha", "@nocobase/plugin-verification": "1.4.0-alpha", "@nocobase/plugin-workflow": "1.4.0-alpha", "@nocobase/plugin-workflow-action-trigger": "1.4.0-alpha", diff --git a/packages/presets/nocobase/src/server/index.ts b/packages/presets/nocobase/src/server/index.ts index 44d8a948ed..49e2c0aed9 100644 --- a/packages/presets/nocobase/src/server/index.ts +++ b/packages/presets/nocobase/src/server/index.ts @@ -22,6 +22,7 @@ export class PresetNocoBase extends Plugin { 'field-sequence', 'verification', 'users', + 'user-data-sync', 'acl', 'field-china-region', 'workflow', From 472190b7d5b974c58cf9762e4f13c6b01ea5733c Mon Sep 17 00:00:00 2001 From: Zhi Chen <350447433@qq.com> Date: Mon, 2 Sep 2024 09:10:19 +0800 Subject: [PATCH 03/68] feat: user sync support custom fields and department owner (#5158) --- .../src/server/user-data-resource-manager.ts | 7 ++- .../src/server/__tests__/data-sync.test.ts | 48 +++++++++++++++++ .../plugin-users/src/server/server.ts | 2 +- .../src/server/user-data-sync-resource.ts | 52 +++++++++++-------- 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts index fa0aac086e..89a2bc3a5d 100644 --- a/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts +++ b/packages/plugins/@nocobase/plugin-user-data-sync/src/server/user-data-resource-manager.ts @@ -17,7 +17,7 @@ export type FormatUser = { email?: string; nickname?: string; phone?: string; - departments?: string[]; + departments?: (string | FormatUserDepartment)[]; isDeleted?: boolean; [key: string]: any; }; @@ -30,6 +30,11 @@ export type FormatDepartment = { [key: string]: any; }; +export type FormatUserDepartment = { + uid: string; + isOwner?: boolean; +}; + export type UserDataRecord = FormatUser | FormatDepartment; export type SyncDataType = 'user' | 'department'; diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts index 816925a7dd..0b1d2f99c0 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/data-sync.test.ts @@ -119,4 +119,52 @@ describe('user data sync', () => { expect(user2).toBeTruthy(); expect(user2.nickname).toBe('test2'); }); + + it('should update user custom field', async () => { + const userCollection = db.getCollection('users'); + userCollection.addField('customField', { type: 'string' }); + await db.sync({ + alter: true, + }); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test', + email: 'test@nocobase.com', + customField: 'test', + }, + ], + }); + const user = await db.getRepository('users').findOne({ + filter: { + email: 'test@nocobase.com', + }, + }); + expect(user).toBeTruthy(); + expect(user.customField).toBe('test'); + await resourceManager.updateOrCreate({ + sourceName: 'test', + dataType: 'user', + matchKey: 'email', + records: [ + { + uid: '1', + nickname: 'test', + email: 'test@nocobase.com', + customField: 'test2', + }, + ], + }); + const user2 = await db.getRepository('users').findOne({ + filter: { + id: user.id, + }, + }); + expect(user2).toBeTruthy(); + expect(user2.customField).toBe('test2'); + }); }); diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index ac4d218e0b..1034775186 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -182,7 +182,7 @@ export default class PluginUsersServer extends Plugin { }); const userDataSyncPlugin = this.app.pm.get('user-data-sync') as PluginUserDataSyncServer; - if (userDataSyncPlugin) { + if (userDataSyncPlugin && userDataSyncPlugin.enabled) { userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger)); } } diff --git a/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts b/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts index ec9137059b..02638e63fc 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/user-data-sync-resource.ts @@ -8,6 +8,8 @@ */ import { Model } from '@nocobase/database'; +import lodash from 'lodash'; + import { FormatUser, OriginRecord, @@ -24,7 +26,24 @@ export class UserDataSyncResource extends UserDataResource { get userRepo() { return this.db.getRepository('users'); } - + getFlteredSourceUser(sourceUser: FormatUser) { + const deleteProps = [ + 'id', + 'uid', + 'createdAt', + 'updatedAt', + 'appLang', + 'resetToken', + 'systemSettings', + 'password', + 'sort', + 'createdById', + 'updatedById', + 'isDeleted', + 'departments', + ]; + return lodash.omit(sourceUser, deleteProps); + } async updateUser(user: Model, sourceUser: FormatUser) { if (sourceUser.isDeleted) { // 删除用户 @@ -39,22 +58,13 @@ export class UserDataSyncResource extends UserDataResource { return; } let dataChanged = false; - if (sourceUser.username !== undefined && user.username !== sourceUser.username) { - user.username = sourceUser.username; - dataChanged = true; - } - if (sourceUser.phone !== undefined && user.phone !== sourceUser.phone) { - user.phone = sourceUser.phone; - dataChanged = true; - } - if (sourceUser.email !== undefined && user.email !== sourceUser.email) { - user.email = sourceUser.email; - dataChanged = true; - } - if (sourceUser.nickname !== undefined && user.nickname !== sourceUser.nickname) { - user.nickname = sourceUser.nickname; - dataChanged = true; - } + const filteredSourceUser = this.getFlteredSourceUser(sourceUser); + lodash.forOwn(filteredSourceUser, (value, key) => { + if (user[key] !== value) { + user[key] = value; + dataChanged = true; + } + }); if (dataChanged) { await user.save(); } @@ -88,13 +98,9 @@ export class UserDataSyncResource extends UserDataResource { if (user) { await this.updateUser(user, sourceUser); } else { + const filteredSourceUser = this.getFlteredSourceUser(sourceUser); user = await this.userRepo.create({ - values: { - nickname: sourceUser.nickname, - phone: sourceUser.phone, - email: sourceUser.email, - username: sourceUser.username, - }, + values: filteredSourceUser, }); } return [{ resourcesPk: user.id, isDeleted: false }]; From c77cdf34b9ea06f1bb878792de85ff01586aea15 Mon Sep 17 00:00:00 2001 From: Katherine Date: Mon, 2 Sep 2024 13:15:04 +0800 Subject: [PATCH 04/68] chore: configuration of collection field default value (#5047) * chore: configure collection field defalt value * chore: getDefaultValueProps * chore: collectionFieldInterface * refactor: defaultProps * refactor: defaultProps * revert: field default props * revert: field default props * revert: field default props * revert: field default props --- .../Configuration/AddFieldAction.tsx | 47 +------------ .../Configuration/EditFieldAction.tsx | 66 ++++--------------- .../CollectionFieldInterface.ts | 65 +++++++++++++++++- 3 files changed, 78 insertions(+), 100 deletions(-) diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 622e5d830b..0fb0ad2965 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -16,64 +16,23 @@ import { cloneDeep } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRequest } from '../../api-client'; +import { CollectionFieldInterface } from '../../data-source'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; import { useResourceActionContext, useResourceContext } from '../ResourceActionProvider'; import { useCancelAction } from '../action-hooks'; import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; -import { IField } from '../interfaces/types'; import * as components from './components'; import { useFieldInterfaceOptions } from './interfaces'; -const getSchema = (schema: IField, record: any, compile) => { +const getSchema = (schema: CollectionFieldInterface, record: any, compile) => { if (!schema) { return; } - const properties = cloneDeep(schema.properties) as any; + const properties = schema.getConfigureFormProperties(); - if (schema.hasDefaultValue === true) { - properties['defaultValue'] = cloneDeep(schema?.default?.uiSchema); - properties.defaultValue.required = false; - properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); - properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = [ - { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', - }, - }, - }, - }, - { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', - fulfill: { - state: { - hidden: true, - value: null, - }, - }, - otherwise: { - state: { - hidden: false, - }, - }, - }, - ]; - } const initialValue: any = { name: `f_${uid()}`, ...cloneDeep(schema.default), diff --git a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx index db509e36b6..3ca1486ac1 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditFieldAction.tsx @@ -16,6 +16,7 @@ import set from 'lodash/set'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useRequest } from '../../api-client'; +import { CollectionFieldInterface } from '../../data-source'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { RecordProvider, useRecord } from '../../record-provider'; import { ActionContextProvider, SchemaComponent, useActionContext, useCompile } from '../../schema-component'; @@ -23,60 +24,23 @@ import { useResourceActionContext, useResourceContext } from '../ResourceActionP import { useCancelAction } from '../action-hooks'; import { useCollectionManager_deprecated } from '../hooks'; import useDialect from '../hooks/useDialect'; -import { IField } from '../interfaces/types'; import * as components from './components'; -const getSchema = (schema: IField, record: any, compile, getContainer): ISchema => { +const getSchema = ( + schema: CollectionFieldInterface, + defaultValues: any, + record: any, + compile, + getContainer, +): ISchema => { if (!schema) { return; } - const properties = cloneDeep(schema.properties) as any; + const properties = schema.getConfigureFormProperties(); if (properties?.name) { properties.name['x-disabled'] = true; } - if (schema.hasDefaultValue === true) { - properties['defaultValue'] = cloneDeep(schema.default.uiSchema) || {}; - properties.defaultValue.required = false; - properties['defaultValue']['title'] = compile('{{ t("Default value") }}'); - properties['defaultValue']['x-decorator'] = 'FormItem'; - properties['defaultValue']['x-reactions'] = [ - { - dependencies: [ - 'uiSchema.x-component-props.gmt', - 'uiSchema.x-component-props.showTime', - 'uiSchema.x-component-props.dateFormat', - 'uiSchema.x-component-props.timeFormat', - ], - fulfill: { - state: { - componentProps: { - gmt: '{{$deps[0]}}', - showTime: '{{$deps[1]}}', - dateFormat: '{{$deps[2]}}', - timeFormat: '{{$deps[3]}}', - }, - }, - }, - }, - { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', - fulfill: { - state: { - hidden: true, - value: undefined, - }, - }, - otherwise: { - state: { - hidden: false, - }, - }, - }, - ]; - } - return { type: 'object', properties: { @@ -92,7 +56,7 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema return useRequest( () => Promise.resolve({ - data: cloneDeep(omit(schema.default, ['uiSchema.rawTitle'])), + data: cloneDeep(omit(defaultValues, ['uiSchema.rawTitle'])), }), options, ); @@ -230,15 +194,7 @@ export const EditFieldAction = (props) => { set(defaultValues.reverseField, 'name', `f_${uid()}`); set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title); } - const schema = getSchema( - { - ...interfaceConf, - default: defaultValues, - }, - record, - compile, - getContainer, - ); + const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer); setSchema(schema); setVisible(true); }} diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index c063a2a577..16cb0c008c 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -8,9 +8,10 @@ */ import type { ISchema } from '@formily/react'; +import { cloneDeep } from 'lodash'; import type { CollectionFieldOptions } from '../collection'; import { CollectionFieldInterfaceManager } from './CollectionFieldInterfaceManager'; - +import { defaultProps } from '../../collection-manager/interfaces/properties'; export type CollectionFieldInterfaceFactory = new ( collectionFieldInterfaceManager: CollectionFieldInterfaceManager, ) => CollectionFieldInterface; @@ -42,6 +43,7 @@ export abstract class CollectionFieldInterface { componentOptions?: CollectionFieldInterfaceComponentOption[]; isAssociation?: boolean; operators?: any[]; + properties?: any; /** * - 如果该值为空,则在 Filter 组件中该字段会被过滤掉 * - 如果该值为空,则不会在变量列表中看到该字段 @@ -82,4 +84,65 @@ export abstract class CollectionFieldInterface { } this.componentOptions.push(componentOption); } + getConfigureFormProperties() { + const defaultValueProps = this.hasDefaultValue ? this.getDefaultValueProperty() : {}; + return { + ...cloneDeep({ ...defaultProps, ...this?.properties }), + ...defaultValueProps, + }; + } + getDefaultValueProperty() { + return { + defaultValue: { + ...cloneDeep(this?.default?.uiSchema), + ...this?.properties?.uiSchema, + required: false, + title: '{{ t("Default value") }}', + 'x-decorator': 'FormItem', + 'x-reactions': [ + { + dependencies: [ + 'uiSchema.x-component-props.gmt', + 'uiSchema.x-component-props.showTime', + 'uiSchema.x-component-props.dateFormat', + 'uiSchema.x-component-props.timeFormat', + ], + fulfill: { + state: { + componentProps: { + gmt: '{{$deps[0]}}', + showTime: '{{$deps[1]}}', + dateFormat: '{{$deps[2]}}', + timeFormat: '{{$deps[3]}}', + }, + }, + }, + }, + { + dependencies: ['primaryKey', 'unique', 'autoIncrement'], + when: '{{$deps[0]||$deps[1]||$deps[2]}}', + fulfill: { + state: { + hidden: true, + value: null, + }, + }, + otherwise: { + state: { + hidden: false, + }, + }, + }, + { + dependencies: ['uiSchema.enum'], + fulfill: { + state: { + dataSource: '{{$deps[0]}}', + }, + }, + }, + ], + }, + }; + } } From 41de0d8c453d63ce2b0a6233947e7efd08511a7f Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Tue, 3 Sep 2024 12:55:45 +0800 Subject: [PATCH 05/68] chore: tag next (#5177) * chore: tag and release next * chore: comment release command * chore: update ci --- .github/workflows/release-next.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index 5ba6d7b8af..d69840caa1 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -75,8 +75,8 @@ jobs: - name: publish npmjs.org continue-on-error: true run: | - git config --global user.email "test@mail.com" - git config --global user.name "test" + git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>' git config --global --add safe.directory /__w/nocobase/nocobase npm config set access public npm config set registry https://registry.npmjs.org/ @@ -86,6 +86,8 @@ jobs: yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} yarn install yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version + git tag ${{ env.NEWVERSION }} + git push origin ${{ env.NEWVERSION }} yarn build echo "# test" >> Release.md git add . @@ -106,11 +108,19 @@ jobs: do git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done - - name: Build Pro plugins + - name: Tag and Build Pro plugins run: | yarn config set registry https://registry.npmjs.org/ yarn install yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version + cd packages/pro-plugins + git tag ${{ env.NEWVERSION }} + git push origin ${{ env.NEWVERSION }} + for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} + do + git tag ${{ env.NEWVERSION }} + git push origin ${{ env.NEWVERSION }} + done yarn build packages/pro-plugins - name: publish pkg.nocobase.com run: | From 0e1e493b4c5c025632bcd3247cadc1c330e1ac41 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Tue, 3 Sep 2024 13:03:20 +0800 Subject: [PATCH 06/68] fix(ci): release next --- .github/workflows/release-next.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index d69840caa1..23606e2de3 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -108,11 +108,8 @@ jobs: do git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo done - - name: Tag and Build Pro plugins + - name: Tag Pro plugins run: | - yarn config set registry https://registry.npmjs.org/ - yarn install - yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version cd packages/pro-plugins git tag ${{ env.NEWVERSION }} git push origin ${{ env.NEWVERSION }} @@ -121,6 +118,11 @@ jobs: git tag ${{ env.NEWVERSION }} git push origin ${{ env.NEWVERSION }} done + - name: Tag and Build Pro plugins + run: | + yarn config set registry https://registry.npmjs.org/ + yarn install + yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version yarn build packages/pro-plugins - name: publish pkg.nocobase.com run: | From e1ca0a09279364a12ac7e2f215953397dc6cebd9 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Tue, 3 Sep 2024 13:27:19 +0800 Subject: [PATCH 07/68] fix(ci): release next --- .github/workflows/release-next.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index 23606e2de3..a3211c9d10 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -110,15 +110,18 @@ jobs: done - name: Tag Pro plugins run: | - cd packages/pro-plugins + cd ./packages/pro-plugins git tag ${{ env.NEWVERSION }} git push origin ${{ env.NEWVERSION }} + cd ../../ for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} do - git tag ${{ env.NEWVERSION }} - git push origin ${{ env.NEWVERSION }} + cd ./packages/pro-plugins/@nocobase/$repo + git tag ${{ env.NEWVERSION }} + git push origin ${{ env.NEWVERSION }} + cd ../../../../ done - - name: Tag and Build Pro plugins + - name: Build Pro plugins run: | yarn config set registry https://registry.npmjs.org/ yarn install From dfe517e737aa3a1647998bb613c78e882bed990a Mon Sep 17 00:00:00 2001 From: chenos Date: Tue, 3 Sep 2024 14:46:20 +0800 Subject: [PATCH 08/68] chore: update pro dockerfile --- .github/workflows/build-pro-image.yml | 4 +- Dockerfile.pro | 82 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.pro diff --git a/.github/workflows/build-pro-image.yml b/.github/workflows/build-pro-image.yml index b0f9fe29f1..7e37364589 100644 --- a/.github/workflows/build-pro-image.yml +++ b/.github/workflows/build-pro-image.yml @@ -11,7 +11,7 @@ on: - 'next' paths: - 'packages/**' - - 'Dockerfile' + - 'Dockerfile.pro' - '.github/workflows/build-pro-image.yml' jobs: @@ -135,7 +135,7 @@ jobs: uses: docker/build-push-action@v3 with: context: . - file: Dockerfile + file: Dockerfile.pro build-args: | VERDACCIO_URL=http://localhost:4873/ COMMIT_HASH=${GITHUB_SHA} diff --git a/Dockerfile.pro b/Dockerfile.pro new file mode 100644 index 0000000000..dd229f7c85 --- /dev/null +++ b/Dockerfile.pro @@ -0,0 +1,82 @@ +FROM node:20.13-bullseye as builder +ARG VERDACCIO_URL=http://host.docker.internal:10104/ +ARG COMMIT_HASH +ARG APPEND_PRESET_LOCAL_PLUGINS +ARG BEFORE_PACK_NOCOBASE="ls -l" +ARG PLUGINS_DIRS +ARG PG_CLIENT_VERSION="16.1" + +ENV PLUGINS_DIRS=${PLUGINS_DIRS} + + +RUN npx npm-cli-adduser --username test --password test -e test@nocobase.com -r $VERDACCIO_URL + +RUN apt-get update && apt-get install -y jq +WORKDIR /tmp +COPY . /tmp +RUN yarn install && yarn build --no-dts + +RUN cd /tmp && \ + NEWVERSION="$(cat lerna.json | jq '.version' | tr -d '"').$(date +'%Y%m%d%H%M%S')" \ + && git checkout -b release-$(date +'%Y%m%d%H%M%S') \ + && yarn lerna version ${NEWVERSION} -y --no-git-tag-version +RUN git config user.email "test@mail.com" \ + && git config user.name "test" && git add . \ + && git commit -m "chore(versions): test publish packages" +RUN yarn release:force --registry $VERDACCIO_URL + +RUN yarn config set registry $VERDACCIO_URL +WORKDIR /app +RUN cd /app \ + && yarn config set network-timeout 600000 -g \ + && yarn create nocobase-app my-nocobase-app -a -e APP_ENV=production -e APPEND_PRESET_LOCAL_PLUGINS=$APPEND_PRESET_LOCAL_PLUGINS \ + && cd /app/my-nocobase-app \ + && yarn install --production + +WORKDIR /app/my-nocobase-app +RUN $BEFORE_PACK_NOCOBASE + +RUN cd /app \ + && rm -rf my-nocobase-app/packages/app/client/src/.umi \ + && rm -rf nocobase.tar.gz \ + && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . + +# add database client +FROM debian:11-slim AS dbclient-builder +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + wget \ + dpkg \ + ca-certificates \ + build-essential \ + libreadline-dev \ + zlib1g-dev \ + libicu-dev + +RUN wget https://ftp.postgresql.org/pub/source/v16.0/postgresql-16.0.tar.gz && \ + tar -xzf postgresql-16.0.tar.gz +WORKDIR /postgresql-16.0 +RUN ./configure --without-readline --without-zlib --without-icu && \ + make -C src/bin/pg_dump +RUN wget https://repo.mysql.com/apt/debian/pool/mysql-8.0/m/mysql-community/mysql-community-client-core_8.0.37-1debian11_amd64.deb && \ + dpkg-deb -x mysql-community-client-core_8.0.37-1debian11_amd64.deb /tmp/mysql-community-client + +FROM node:20.13-bullseye-slim +RUN apt-get update && apt-get install -y nginx libpq5 libreadline8 +RUN rm -rf /etc/nginx/sites-enabled/default +COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf +COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz + +WORKDIR /app/nocobase + +RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH + +COPY --from=dbclient-builder /postgresql-16.0/src/bin/pg_dump/pg_dump /usr/local/bin/ +COPY --from=dbclient-builder /postgresql-16.0/src/bin/pg_dump/pg_restore /usr/local/bin/ +COPY --from=dbclient-builder /tmp/mysql-community-client/usr/bin/mysql /usr/local/bin/ +COPY --from=dbclient-builder /tmp/mysql-community-client/usr/bin/mysqldump /usr/local/bin/ + +COPY ./docker/nocobase/docker-entrypoint.sh /app/ + +CMD ["/app/docker-entrypoint.sh"] From 61f95f78c2f15a1c92eef1ff4629a909eaea9854 Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Tue, 3 Sep 2024 14:46:43 +0800 Subject: [PATCH 09/68] feat(data-vi): allow to add refresh button and set auto refresh interval (#5112) * feat(data-vi): allow to set auto refresh interval for charts * feat(data-vi): allow to add refresh button and set auto refresh interval * fix: build * fix: bug * chore: optimize global auto refresh * chore: remove console.log * fix: remove console log --- .../src/client/block/ChartBlock.tsx | 9 +- .../src/client/block/ChartBlockDesigner.tsx | 1 - .../client/block/ChartBlockInitializer.tsx | 11 +++ .../src/client/block/ChartBlockProvider.tsx | 25 +++++ .../src/client/block/ChartDataProvider.tsx | 1 + .../block/GlobalAutoRefreshProvider.tsx | 59 +++++++++++ .../src/client/block/index.ts | 2 + .../client/configure/ChartConfigProvider.tsx | 2 +- .../src/client/index.tsx | 17 +++- .../initializers/BlockRefreshAction.tsx | 95 ++++++++++++++++++ .../src/client/initializers/RefreshAction.tsx | 94 ++++++++++++++++++ .../src/client/initializers/chartActions.tsx | 25 +++++ .../client/initializers/chartBlockActions.tsx | 25 +++++ .../src/client/renderer/ChartRenderer.tsx | 99 ++++--------------- .../client/renderer/ChartRendererDesigner.tsx | 73 ++++++++++++++ .../client/renderer/ChartRendererProvider.tsx | 55 +++++++++-- .../src/client/settings/AutoRefreshItem.tsx | 73 ++++++++++++++ .../client/settings/chartActionRefresh.tsx | 70 +++++++++++++ .../settings/chartBlockActionRefresh.tsx | 70 +++++++++++++ .../src/client/utils.ts | 23 ++++- .../src/locale/en-US.json | 3 +- .../src/locale/zh-CN.json | 3 +- 22 files changed, 743 insertions(+), 92 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlockProvider.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/block/GlobalAutoRefreshProvider.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/BlockRefreshAction.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/RefreshAction.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartActions.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartBlockActions.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererDesigner.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/AutoRefreshItem.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartActionRefresh.tsx create mode 100644 packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartBlockActionRefresh.tsx diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlock.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlock.tsx index 95a7b09a2a..9f7004af38 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlock.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlock.tsx @@ -14,6 +14,7 @@ import { ChartDataProvider } from './ChartDataProvider'; import { ChartRenderer, ChartRendererProvider } from '../renderer'; import { ChartFilterBlockProvider, ChartFilterBlockDesigner } from '../filter'; import { ChartFilterProvider } from '../filter/FilterProvider'; +import { RefreshButton } from '../initializers/RefreshAction'; import { css } from '@emotion/css'; export const ChartV2Block: React.FC = (props) => { @@ -24,7 +25,13 @@ export const ChartV2Block: React.FC = (props) => { value={{ ...schemaInitializerContextData, visible: initialVisible, setVisible: setInitialVisible }} >
{ }, }, 'x-designer': 'ChartV2BlockDesigner', + 'x-decorator': 'ChartBlockProvider', properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-designer-offset)', + }, + }, + 'x-initializer': 'chartBlock:configureActions', + }, [uid()]: { type: 'void', 'x-component': 'Grid', diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlockProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlockProvider.tsx new file mode 100644 index 0000000000..9e035d7a0d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartBlockProvider.tsx @@ -0,0 +1,25 @@ +/** + * 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 React from 'react'; +import { BlockRefreshButton } from '../initializers/BlockRefreshAction'; +import { SchemaComponentOptions } from '@nocobase/client'; +import { GlobalAutoRefreshProvider } from './GlobalAutoRefreshProvider'; + +export const ChartBlockProvider: React.FC = (props) => { + return ( + + {props.children} + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartDataProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartDataProvider.tsx index 7360e6f819..5fdf63afe2 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartDataProvider.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/ChartDataProvider.tsx @@ -34,6 +34,7 @@ export const ChartDataProvider: React.FC = (props) => { const removeChart = useMemoizedFn((uid: string) => { setCharts((charts) => ({ ...charts, [uid]: undefined })); }); + return ( {props.children} ); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/GlobalAutoRefreshProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/GlobalAutoRefreshProvider.tsx new file mode 100644 index 0000000000..08d9a5b832 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/GlobalAutoRefreshProvider.tsx @@ -0,0 +1,59 @@ +/** + * 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 React, { useEffect, useRef, useState } from 'react'; +import { useMemoizedFn } from 'ahooks'; + +export const GlobalAutoRefreshContext = React.createContext<{ + addChart: (uid: string, chart: { service: any }) => void; + removeChart: (uid: string) => void; + autoRefresh: number | boolean; + setAutoRefresh: (autoRefresh: number | boolean) => void; + refreshCharts: () => void; +}>({} as any); + +export const GlobalAutoRefreshProvider: React.FC = (props) => { + const [autoRefresh, setAutoRefresh] = useState(false); + const charts = useRef<{ [uid: string]: { service: any; selfAutoRefresh?: boolean } }>({}); + const addChart = useMemoizedFn((uid: string, { service }) => { + charts.current[uid] = { service }; + }); + const removeChart = useMemoizedFn((uid: string) => { + const chart = charts.current[uid]; + if (!chart) { + return; + } + charts.current[uid] = { service: chart.service, selfAutoRefresh: true }; + }); + const refreshCharts = useMemoizedFn(() => { + for (const chart of Object.values(charts.current)) { + chart?.service.refresh(); + } + }); + useEffect(() => { + if (!autoRefresh) { + return; + } + const timer = setInterval( + () => { + const refreshCharts = Object.values(charts.current).filter((chart) => !chart.selfAutoRefresh); + for (const chart of refreshCharts) { + chart?.service.refresh(); + } + }, + (autoRefresh as number) * 1000, + ); + return () => clearInterval(timer); + }, [autoRefresh]); + return ( + + {props.children} + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/index.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/index.ts index e9048ca4af..1a00061fe7 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/index.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/block/index.ts @@ -10,3 +10,5 @@ export * from './ChartBlock'; export * from './ChartBlockDesigner'; export * from './ChartBlockInitializer'; +export * from './ChartBlockProvider'; +export * from './GlobalAutoRefreshProvider'; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/configure/ChartConfigProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/configure/ChartConfigProvider.tsx index 75fe81736d..48b8abac78 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/configure/ChartConfigProvider.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/configure/ChartConfigProvider.tsx @@ -42,7 +42,7 @@ export const ChartConfigProvider: React.FC = (props) => { return ( {props.children} - + insertAdjacent('beforeEnd', schema, options)} /> diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/index.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/index.tsx index 027a0cd2e3..70c945c3f5 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/index.tsx @@ -12,6 +12,7 @@ import { ChartV2Block, ChartV2BlockDesigner, ChartV2BlockInitializer, + ChartBlockProvider, chartInitializers, chartInitializers_deprecated, } from './block'; @@ -25,6 +26,12 @@ import { chartFilterItemInitializers_deprecated, } from './filter'; import { lang } from './locale'; +import { chartActionsInitializer } from './initializers/chartActions'; +import { chartActionRefreshSettings } from './settings/chartActionRefresh'; +import { useChartRefreshActionProps } from './initializers/RefreshAction'; +import { chartBlockActionsInitializer } from './initializers/chartBlockActions'; +import { useChartBlockRefreshActionProps } from './initializers/BlockRefreshAction'; +import { chartBlockActionRefreshSettings } from './settings/chartBlockActionRefresh'; class PluginDataVisualiztionClient extends Plugin { public charts: ChartGroup = new ChartGroup(); @@ -36,14 +43,22 @@ class PluginDataVisualiztionClient extends Plugin { ChartV2BlockInitializer, ChartV2BlockDesigner, ChartV2Block, + ChartBlockProvider, + }); + this.app.addScopes({ + useChartRefreshActionProps, + useChartBlockRefreshActionProps, }); - this.app.schemaInitializerManager.add(chartInitializers_deprecated); this.app.schemaInitializerManager.add(chartInitializers); this.app.schemaInitializerManager.add(chartFilterItemInitializers_deprecated); this.app.schemaInitializerManager.add(chartFilterItemInitializers); this.app.schemaInitializerManager.add(chartFilterActionInitializers_deprecated); this.app.schemaInitializerManager.add(chartFilterActionInitializers); + this.app.schemaInitializerManager.add(chartActionsInitializer); + this.app.schemaInitializerManager.add(chartBlockActionsInitializer); + this.app.schemaSettingsManager.add(chartActionRefreshSettings); + this.app.schemaSettingsManager.add(chartBlockActionRefreshSettings); const blockInitializers = this.app.schemaInitializerManager.get('page:addBlock'); blockInitializers?.add('dataBlocks.chartV2', { diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/BlockRefreshAction.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/BlockRefreshAction.tsx new file mode 100644 index 0000000000..fee51839e9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/BlockRefreshAction.tsx @@ -0,0 +1,95 @@ +/** + * 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 { ActionInitializer } from '@nocobase/client'; +import React, { forwardRef, useContext, useEffect } from 'react'; +import { useChartsTranslation } from '../locale'; +import { Dropdown, MenuProps } from 'antd'; +import { DownOutlined, ReloadOutlined } from '@ant-design/icons'; +import { useFieldSchema } from '@formily/react'; +import { GlobalAutoRefreshContext } from '../block/GlobalAutoRefreshProvider'; + +export const BlockRefreshButton: React.FC = forwardRef((props, ref) => { + const { t } = useChartsTranslation(); + const { autoRefresh, setAutoRefresh } = useContext(GlobalAutoRefreshContext); + const interval = { + 5: '5s', + 10: '10s', + 30: '30s', + 60: '1m', + 300: '5m', + 900: '15m', + 1800: '30m', + 3600: '1h', + 7200: '2h', + 86400: '1d', + }; + const items: MenuProps['items'] = Object.entries(interval).map(([key, label]) => ({ + key, + label, + onClick: () => setAutoRefresh(+key), + })); + return ( + setAutoRefresh(false), + }, + ...items, + ], + }} + icon={} + buttonsRender={([_, rightButton]) => [ + _, + React.cloneElement( + rightButton as React.ReactElement, + { iconPosition: 'end' }, + autoRefresh ? interval[autoRefresh as number] : null, + ), + ]} + > + + {t('Refresh')} + {props.children} + + ); +}); + +export const useChartBlockRefreshActionProps = () => { + const fieldSchema = useFieldSchema(); + const { setAutoRefresh, refreshCharts } = useContext(GlobalAutoRefreshContext); + useEffect(() => { + setAutoRefresh(fieldSchema['x-decorator-props']?.autoRefresh); + return () => { + setAutoRefresh(false); + }; + }, [fieldSchema, setAutoRefresh]); + return { + onClick: () => { + refreshCharts?.(); + }, + }; +}; + +export const BlockRefreshActionInitializer = (props) => { + const schema = { + 'x-component': 'Action', + 'x-use-component-props': 'useChartBlockRefreshActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'chartBlockActionSettings:refresh', + 'x-component-props': { + component: 'BlockRefreshButton', + }, + }; + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/RefreshAction.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/RefreshAction.tsx new file mode 100644 index 0000000000..608da85060 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/RefreshAction.tsx @@ -0,0 +1,94 @@ +/** + * 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 React, { forwardRef, useContext, useEffect } from 'react'; +import { ActionInitializer, useDesignable } from '@nocobase/client'; +import { ChartRendererContext } from '../renderer'; +import { Dropdown, MenuProps } from 'antd'; +import { DownOutlined, ReloadOutlined } from '@ant-design/icons'; +import { useFieldSchema } from '@formily/react'; +import { useChartsTranslation } from '../locale'; + +export const RefreshButton: React.FC = forwardRef((props, ref) => { + const { t } = useChartsTranslation(); + const { autoRefresh, setAutoRefresh, showActionBar } = useContext(ChartRendererContext); + const { designable } = useDesignable(); + const interval = { + 5: '5s', + 10: '10s', + 30: '30s', + 60: '1m', + 300: '5m', + 900: '15m', + 1800: '30m', + 3600: '1h', + 7200: '2h', + 86400: '1d', + }; + const items: MenuProps['items'] = Object.entries(interval).map(([key, label]) => ({ + key, + label, + onClick: () => setAutoRefresh(+key), + })); + return showActionBar || designable ? ( + setAutoRefresh(false), + }, + ...items, + ], + }} + icon={} + buttonsRender={([_, rightButton]) => [ + _, + React.cloneElement( + rightButton as React.ReactElement, + { iconPosition: 'end' }, + autoRefresh ? interval[autoRefresh as number] : null, + ), + ]} + > + + {props.children} + + ) : null; +}); + +export const useChartRefreshActionProps = () => { + const fieldSchema = useFieldSchema(); + const { service, setAutoRefresh } = useContext(ChartRendererContext); + useEffect(() => { + setAutoRefresh(fieldSchema['x-component-props']?.autoRefresh); + return () => { + setAutoRefresh(false); + }; + }, [fieldSchema, setAutoRefresh]); + return { + onClick: service.refresh, + }; +}; + +export const RefreshActionInitializer = (props) => { + const schema = { + 'x-component': 'Action', + 'x-use-component-props': 'useChartRefreshActionProps', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'chartActionSettings:refresh', + 'x-component-props': { + size: 'small', + component: 'RefreshButton', + }, + }; + return ; +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartActions.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartActions.tsx new file mode 100644 index 0000000000..6268c04d56 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartActions.tsx @@ -0,0 +1,25 @@ +/** + * 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 { SchemaInitializer } from '@nocobase/client'; +import { lang } from '../locale'; +import { RefreshActionInitializer } from './RefreshAction'; + +export const chartActionsInitializer = new SchemaInitializer({ + name: 'chart:configureActions', + title: lang('Configure actions'), + icon: 'SettingOutlined', + items: [ + { + name: 'refresh', + title: lang('Refresh'), + Component: RefreshActionInitializer, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartBlockActions.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartBlockActions.tsx new file mode 100644 index 0000000000..83b0acc089 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/initializers/chartBlockActions.tsx @@ -0,0 +1,25 @@ +/** + * 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 { SchemaInitializer } from '@nocobase/client'; +import { lang } from '../locale'; +import { BlockRefreshActionInitializer } from './BlockRefreshAction'; + +export const chartBlockActionsInitializer = new SchemaInitializer({ + name: 'chartBlock:configureActions', + title: lang('Configure actions'), + icon: 'SettingOutlined', + items: [ + { + name: 'refresh', + title: lang('Refresh'), + Component: BlockRefreshActionInitializer, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRenderer.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRenderer.tsx index 913d96f5d2..adfa3def36 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRenderer.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRenderer.tsx @@ -7,32 +7,35 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { useField, useFieldSchema } from '@formily/react'; -import { - GeneralSchemaDesigner, - SchemaSettingsBlockTitleItem, - SchemaSettingsDivider, - SchemaSettingsItem, - SchemaSettingsRemove, - gridRowColWrap, - useAPIClient, - useCollection_deprecated, - useDataSource, - useDesignable, -} from '@nocobase/client'; +import { useAPIClient } from '@nocobase/client'; import { Empty, Result, Spin, Typography } from 'antd'; -import React, { useContext, useEffect, useRef } from 'react'; +import React, { useContext } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { ChartConfigContext } from '../configure'; import { useData, useFieldTransformer, useFieldsWithAssociation } from '../hooks'; import { useChartsTranslation } from '../locale'; -import { createRendererSchema, getField } from '../utils'; +import { getField } from '../utils'; import { ChartRendererContext } from './ChartRendererProvider'; import { useChart } from '../chart/group'; -import { ChartDataContext } from '../block/ChartDataProvider'; import { Schema } from '@formily/react'; +import { ChartRendererDesigner } from './ChartRendererDesigner'; const { Paragraph, Text } = Typography; +const ErrorFallback = ({ error }) => { + const { t } = useChartsTranslation(); + + return ( +
+ + + + {error.message} + + + +
+ ); +}; + export const ChartRenderer: React.FC & { Designer: React.FC; } = (props) => { @@ -84,64 +87,4 @@ export const ChartRenderer: React.FC & { ); }; -ChartRenderer.Designer = function Designer() { - const { t } = useChartsTranslation(); - const { setVisible, setCurrent } = useContext(ChartConfigContext); - const { removeChart } = useContext(ChartDataContext); - const { service } = useContext(ChartRendererContext); - const field = useField(); - const schema = useFieldSchema(); - const { insertAdjacent } = useDesignable(); - const dataSource = useDataSource(); - const { name, title } = useCollection_deprecated(); - return ( - - { - setCurrent({ schema, field, dataSource: dataSource.key, collection: name, service, data: service.data }); - setVisible(true); - }} - > - {t('Configure')} - - insertAdjacent('afterEnd', gridRowColWrap(createRendererSchema(schema?.['x-decorator-props'])))} - > - {t('Duplicate')} - - {/* */} - - { - removeChart(schema['x-uid']); - }, - }} - /> - - ); -}; - -const ErrorFallback = ({ error }) => { - const { t } = useChartsTranslation(); - - return ( -
- - - - {error.message} - - - -
- ); -}; +ChartRenderer.Designer = ChartRendererDesigner; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererDesigner.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererDesigner.tsx new file mode 100644 index 0000000000..0d344c20be --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererDesigner.tsx @@ -0,0 +1,73 @@ +/** + * 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 { useField, useFieldSchema } from '@formily/react'; +import { + GeneralSchemaDesigner, + SchemaSettingsDivider, + SchemaSettingsItem, + SchemaSettingsRemove, + SchemaSettingsSelectItem, + gridRowColWrap, + useCollection_deprecated, + useDataSource, + useDesignable, +} from '@nocobase/client'; +import React, { useContext } from 'react'; +import { ChartConfigContext } from '../configure'; +import { useChartsTranslation } from '../locale'; +import { createRendererSchema } from '../utils'; +import { ChartRendererContext } from './ChartRendererProvider'; +import { ChartDataContext } from '../block/ChartDataProvider'; + +export function ChartRendererDesigner() { + const { t } = useChartsTranslation(); + const { setVisible, setCurrent } = useContext(ChartConfigContext); + const { removeChart } = useContext(ChartDataContext); + const { service } = useContext(ChartRendererContext); + const field = useField(); + const schema = useFieldSchema(); + const { insertAdjacent } = useDesignable(); + const dataSource = useDataSource(); + const { name, title } = useCollection_deprecated(); + return ( + + { + setCurrent({ schema, field, dataSource: dataSource.key, collection: name, service, data: service.data }); + setVisible(true); + }} + > + {t('Configure')} + + insertAdjacent('afterEnd', gridRowColWrap(createRendererSchema(schema?.['x-decorator-props'])))} + > + {t('Duplicate')} + + {/* */} + + { + removeChart(schema['x-uid']); + }, + }} + /> + + ); +} diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx index 1aa8bf9963..e2f22ec5d9 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/renderer/ChartRendererProvider.tsx @@ -13,16 +13,14 @@ import { DEFAULT_DATA_SOURCE_KEY, MaybeCollectionProvider, useAPIClient, - useDataSourceManager, - useParsedFilter, useRequest, } from '@nocobase/client'; -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect } from 'react'; import { parseField, removeUnparsableFilter } from '../utils'; import { ChartDataContext } from '../block/ChartDataProvider'; -import { ConfigProvider } from 'antd'; import { useChartFilter } from '../hooks'; import { ChartFilterContext } from '../filter/FilterProvider'; +import { GlobalAutoRefreshContext } from '../block/GlobalAutoRefreshProvider'; export type MeasureProps = { field: string | string[]; @@ -66,26 +64,36 @@ export type ChartRendererProps = { chartType: string; general: any; advanced: any; + title?: string; + bordered?: boolean; }; transform?: TransformProps[]; mode?: 'builder' | 'sql'; + disableAutoRefresh?: boolean; }; export const ChartRendererContext = createContext< { service: any; data?: any[]; + autoRefresh?: number | boolean; + setAutoRefresh?: (autoRefresh: number | boolean) => void; + showActionBar?: boolean; } & ChartRendererProps >({} as any); ChartRendererContext.displayName = 'ChartRendererContext'; export const ChartRendererProvider: React.FC = (props) => { - const { query, config, collection, transform, dataSource = DEFAULT_DATA_SOURCE_KEY } = props; + const { query, config, collection, transform, dataSource = DEFAULT_DATA_SOURCE_KEY, disableAutoRefresh } = props; const { addChart } = useContext(ChartDataContext); + const { addChart: addGlobalAutoRefreshChart, removeChart: removeGlobalAutoRefreshChart } = + useContext(GlobalAutoRefreshContext); const { ready, form, enabled } = useContext(ChartFilterContext); const { getFilter, hasFilter, appendFilter, parseFilter } = useChartFilter(); const schema = useFieldSchema(); const api = useAPIClient(); + const [autoRefresh, setAutoRefresh] = React.useState(false); + const [showActionBar, setShowActionBar] = React.useState(false); const service = useRequest( async (dataSource, collection, query, manual) => { if (!(collection && query?.measures?.length)) return; @@ -133,6 +141,9 @@ export const ChartRendererProvider: React.FC = (props) => { } finally { if (!manual && schema?.['x-uid']) { addChart(schema?.['x-uid'], { dataSource, collection, service, query }); + if (!autoRefresh) { + addGlobalAutoRefreshChart?.(schema?.['x-uid'], { service }); + } } } }, @@ -144,11 +155,41 @@ export const ChartRendererProvider: React.FC = (props) => { }, ); + useEffect(() => { + if (disableAutoRefresh) { + return; + } + if (!autoRefresh) { + addGlobalAutoRefreshChart?.(schema?.['x-uid'], { service }); + return; + } + removeGlobalAutoRefreshChart?.(schema?.['x-uid']); + const refresh = autoRefresh as number; + const timer = setInterval(service.refresh, refresh * 1000); + return () => { + clearInterval(timer); + }; + }, [autoRefresh, disableAutoRefresh]); + return ( - - {props.children} + +
setShowActionBar(true)} onMouseOut={() => setShowActionBar(false)}> + {props.children} +
diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/AutoRefreshItem.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/AutoRefreshItem.tsx new file mode 100644 index 0000000000..c1dfcce65c --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/AutoRefreshItem.tsx @@ -0,0 +1,73 @@ +/** + * 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 { useField } from '@formily/react'; +import { SchemaSettingsSelectItem } from '@nocobase/client'; +import React from 'react'; +import { useChartsTranslation } from '../locale'; + +export const AutoRefreshItem: React.FC<{ + value: number | boolean; + onChange?: (value: any) => void; +}> = (props) => { + const { t } = useChartsTranslation(); + return ( + + ); +}; diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartActionRefresh.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartActionRefresh.tsx new file mode 100644 index 0000000000..f911e08427 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartActionRefresh.tsx @@ -0,0 +1,70 @@ +/** + * 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 React, { useContext } from 'react'; +import { SchemaSettings, useDesignable } from '@nocobase/client'; +import { useChartsTranslation } from '../locale'; +import { AutoRefreshItem } from './AutoRefreshItem'; +import { useField, useFieldSchema } from '@formily/react'; +import { ChartRendererContext } from '../renderer'; + +export const chartActionRefreshSettings = new SchemaSettings({ + name: 'chartActionSettings:refresh', + items: [ + { + name: 'refresh', + Component: () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { setAutoRefresh } = useContext(ChartRendererContext); + return ( + { + setAutoRefresh(v); + field.componentProps = { + ...field.componentProps, + autoRefresh: v, + }; + fieldSchema['x-component-props'] = field.componentProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': field.componentProps, + }, + }); + dn.refresh(); + }} + /> + ); + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + const { t } = useChartsTranslation(); + return { + removeParentsIfNoChildren: true, + breakRemoveOn: (s) => { + return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar'); + }, + confirm: { + title: t('Delete action'), + }, + }; + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartBlockActionRefresh.tsx b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartBlockActionRefresh.tsx new file mode 100644 index 0000000000..57f641efd1 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/settings/chartBlockActionRefresh.tsx @@ -0,0 +1,70 @@ +/** + * 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 React, { useContext } from 'react'; +import { SchemaSettings, useDesignable } from '@nocobase/client'; +import { useChartsTranslation } from '../locale'; +import { AutoRefreshItem } from './AutoRefreshItem'; +import { useField, useFieldSchema } from '@formily/react'; +import { GlobalAutoRefreshContext } from '../block'; + +export const chartBlockActionRefreshSettings = new SchemaSettings({ + name: 'chartBlockActionSettings:refresh', + items: [ + { + name: 'refresh', + Component: () => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { dn } = useDesignable(); + const { setAutoRefresh } = useContext(GlobalAutoRefreshContext); + return ( + { + setAutoRefresh(v); + field.decoratorProps = { + ...field.decoratorProps, + autoRefresh: v, + }; + fieldSchema['x-decorator-props'] = field.decoratorProps; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-decorator-props': field.decoratorProps, + }, + }); + dn.refresh(); + }} + /> + ); + }, + }, + { + name: 'divider', + type: 'divider', + }, + { + name: 'delete', + type: 'remove', + useComponentProps() { + const { t } = useChartsTranslation(); + return { + removeParentsIfNoChildren: true, + breakRemoveOn: (s) => { + return s['x-component'] === 'Space' || s['x-component'].endsWith('ActionBar'); + }, + confirm: { + title: t('Delete action'), + }, + }; + }, + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/client/utils.ts b/packages/plugins/@nocobase/plugin-data-visualization/src/client/utils.ts index a823640fa5..43701e5a42 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/client/utils.ts +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/client/utils.ts @@ -12,7 +12,8 @@ import { uid } from '@formily/shared'; import lodash from 'lodash'; import { SelectedField } from './configure'; import { FieldOption } from './hooks'; -import { QueryProps } from './renderer'; +import { ChartRendererContext, QueryProps } from './renderer'; +import { useContext } from 'react'; export const createRendererSchema = (decoratorProps: any, componentProps = {}) => { const { collection, config } = decoratorProps; @@ -31,6 +32,26 @@ export const createRendererSchema = (decoratorProps: any, componentProps = {}) = }, 'x-initializer': 'charts:addBlock', properties: { + actions: { + type: 'void', + 'x-decorator': 'div', + 'x-decorator-props': { + style: { + position: 'absolute', + top: 0, + right: 0, + zIndex: 10, + }, + }, + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginRight: 'var(--nb-designer-offset)', + marginTop: 'var(--nb-designer-offset)', + }, + }, + 'x-initializer': 'chart:configureActions', + }, [uid()]: { type: 'void', 'x-component': 'ChartRenderer', diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-data-visualization/src/locale/en-US.json index 7a6b127771..2abce3cad8 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/locale/en-US.json @@ -95,5 +95,6 @@ "Fixed height": "Fixed height", "Show background": "Show background", "Show padding": "Show padding", - "Distinct": "Distinct" + "Distinct": "Distinct", + "Auto refresh": "Auto refresh" } diff --git a/packages/plugins/@nocobase/plugin-data-visualization/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-data-visualization/src/locale/zh-CN.json index 563357e91f..811d8a6ca7 100644 --- a/packages/plugins/@nocobase/plugin-data-visualization/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-data-visualization/src/locale/zh-CN.json @@ -96,5 +96,6 @@ "Fixed height": "固定高度", "Show background": "显示背景", "Show padding": "显示内边距", - "Distinct": "去重" + "Distinct": "去重", + "Auto refresh": "自动刷新" } From 0660a077e8e873678520e9a70e695565ee97ebd6 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Tue, 3 Sep 2024 16:34:20 +0800 Subject: [PATCH 10/68] fix(ci): release-next tag name --- .github/workflows/release-next.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-next.yml b/.github/workflows/release-next.yml index a3211c9d10..1963cc2df6 100644 --- a/.github/workflows/release-next.yml +++ b/.github/workflows/release-next.yml @@ -86,8 +86,8 @@ jobs: yarn config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} yarn install yarn lerna version ${{ env.NEWVERSION }} -y --no-git-tag-version - git tag ${{ env.NEWVERSION }} - git push origin ${{ env.NEWVERSION }} + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} yarn build echo "# test" >> Release.md git add . @@ -111,14 +111,14 @@ jobs: - name: Tag Pro plugins run: | cd ./packages/pro-plugins - git tag ${{ env.NEWVERSION }} - git push origin ${{ env.NEWVERSION }} + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} cd ../../ for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }} do cd ./packages/pro-plugins/@nocobase/$repo - git tag ${{ env.NEWVERSION }} - git push origin ${{ env.NEWVERSION }} + git tag v${{ env.NEWVERSION }} + git push origin v${{ env.NEWVERSION }} cd ../../../../ done - name: Build Pro plugins From 93a788e7699ae7e84dc7b51382fda7fe93700313 Mon Sep 17 00:00:00 2001 From: Katherine Date: Thu, 5 Sep 2024 23:01:41 +0800 Subject: [PATCH 11/68] refactor: ActionPanel support popup & custom request action (#5148) * refactor: workbench support popup action * fix: popup action in workben * fix: popup action in workben * feat(WorkbenchAction): adapt popup * refactor: workbench support custom request * fix: workbench support custom request * refactor: action panel * refactor: support layout * refactor: support layout * refactor: locale improve * refactor: popup action support openMode * refactor: layout * fix: bug * fix: bug * fix: bug * style: improve * fix(map): resolve issue with map not displaying * refactor: add popup action * fix: bug * refactor: confirm title * style: improve * fix: bug * fix: resolve error when creating blocks * chore: get unit test pass --------- Co-authored-by: Zeke Zhang <958414905@qq.com> --- lerna.json | 4 +- .../hooks/useSchemaInitializerRender.tsx | 15 +- .../schema-component/antd/action/Action.tsx | 9 +- .../schema-component/antd/page/PagePopups.tsx | 20 ++- .../src/schema-component/antd/page/index.ts | 2 +- .../antd/page/pagePopupUtils.tsx | 4 +- ...ePopupContextInActionOrAssociationField.ts | 2 +- .../buttons/RecordBlockInitializers.tsx | 14 +- .../hooks/useCustomizeRequestActionProps.ts | 2 +- .../src/client/schemaSettings.ts | 8 +- .../src/client/WorkbenchAction.tsx | 28 +++- .../src/client/WorkbenchBlock.tsx | 72 +++++++-- ...stomRequestActionSchemaInitializerItem.tsx | 97 ++++++++++++ ...kbenchPopupActionSchemaInitializerItem.tsx | 145 ++++++++++++++++++ .../src/client/index.tsx | 30 +++- .../client/workbenchBlockInitializerItem.ts | 2 +- .../src/client/workbenchBlockSettings.ts | 20 --- .../src/client/workbenchBlockSettings.tsx | 62 ++++++++ .../src/locale/zh-CN.json | 11 +- .../plugin-map/src/client/block/MapBlock.tsx | 3 +- .../mobile-action-page/MobileActionPage.tsx | 62 +------- .../mobile-action-page/blockInitializers.ts | 47 ++++++ .../src/client/mobile/Mobile.tsx | 2 + .../src/client/WorkflowManualProvider.tsx | 2 +- 24 files changed, 526 insertions(+), 137 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx create mode 100644 packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx delete mode 100644 packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.ts create mode 100644 packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.tsx create mode 100644 packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/blockInitializers.ts diff --git a/lerna.json b/lerna.json index 0ecbef7cdf..9710afb03d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.4.0-alpha", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx index 968aa3f01d..b05a170aa0 100644 --- a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx +++ b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx @@ -12,9 +12,9 @@ import React, { FC, useMemo } from 'react'; import { useApp } from '../../hooks'; import { SchemaInitializerItems } from '../components'; import { SchemaInitializerButton } from '../components/SchemaInitializerButton'; -import { withInitializer } from '../withInitializer'; -import { SchemaInitializerOptions } from '../types'; import { SchemaInitializer } from '../SchemaInitializer'; +import { SchemaInitializerOptions } from '../types'; +import { withInitializer } from '../withInitializer'; const InitializerComponent: FC> = React.memo((options) => { const Component: any = options.Component || SchemaInitializerButton; @@ -38,6 +38,17 @@ export function useSchemaInitializerRender( options?: Omit, 'name'>, ) { const app = useApp(); + + // compatible with mobile + // TODO: delete this code + if ( + name === 'popup:common:addBlock' && + app.schemaInitializerManager.has('mobile:popup:common:addBlock') && + window.location.pathname.startsWith('/m/') + ) { + name = 'mobile:popup:common:addBlock'; + } + const initializer = useMemo( () => (typeof name === 'object' ? name : app.schemaInitializerManager.get(name)), [app.schemaInitializerManager, name], 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 32676ec10a..898b08cb19 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -72,6 +72,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( addChild, onMouseEnter, refreshDataBlockRequest: propsRefreshDataBlockRequest, + confirmTitle, ...others } = useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const aclCtx = useACLActionParamsContext(); @@ -173,6 +174,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( confirm, modal, setSubmitted: setParentSubmitted, + confirmTitle, }; const buttonElement = RenderButton(buttonProps); @@ -306,6 +308,7 @@ function RenderButton({ confirm, modal, setSubmitted, + confirmTitle, }) { const { t } = useTranslation(); const { isPopupVisibleControlledByURL } = usePopupSettings(); @@ -332,8 +335,8 @@ function RenderButton({ setVisible(true); run?.(); } else { + // Currently, only buttons of these types can control the visibility of popups through URLs. if ( - // Currently, only buttons of these types can control the visibility of popups through URLs. ['view', 'update', 'create', 'customize:popup'].includes(fieldSchema['x-action']) && fieldSchema['x-uid'] ) { @@ -346,8 +349,8 @@ function RenderButton({ }; if (confirm?.content) { modal.confirm({ - title: t(confirm.title, { title: actionTitle }), - content: t(confirm.content, { title: actionTitle }), + title: t(confirm.title, { title: confirmTitle || actionTitle }), + content: t(confirm.content, { title: confirmTitle || actionTitle }), onOk, }); } else { diff --git a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx index e9c52d4161..dc0aa0f44c 100644 --- a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx +++ b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx @@ -141,14 +141,23 @@ const PagePopupsItemProvider: FC<{ const storedContext = { ...getStoredPopupContext(params.popupuid) }; if (!context) { - context = storedContext; + context = _.omitBy( + { + dataSource: storedContext.dataSource, + collection: storedContext.collection, + association: storedContext.association, + }, + _.isNil, + ) as PopupContext; } if (_.isEmpty(context)) { return ( - -
{children}
-
+ + +
{children}
+
+
); } @@ -275,7 +284,9 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => { isSubPage: isSubPageSchema(schema), }; }); + const rootSchema = clonedSchemas[0]; + for (let i = 1; i < clonedSchemas.length; i++) { insertChildToParentSchema({ childSchema: clonedSchemas[i], @@ -289,6 +300,7 @@ export const PagePopups = (props: { paramsList?: PopupParams[] }) => { }, }); } + setRootSchema(rootSchema); }; run(); diff --git a/packages/core/client/src/schema-component/antd/page/index.ts b/packages/core/client/src/schema-component/antd/page/index.ts index 477ed6ef08..59fb46eefd 100644 --- a/packages/core/client/src/schema-component/antd/page/index.ts +++ b/packages/core/client/src/schema-component/antd/page/index.ts @@ -13,6 +13,6 @@ export * from './FixedBlockDesignerItem'; export * from './Page'; export * from './Page.Settings'; export { PagePopups } from './PagePopups'; -export { storePopupContext } from './pagePopupUtils'; +export { storePopupContext, withSearchParams, getPopupPathFromParams } from './pagePopupUtils'; export * from './PageTab.Settings'; export { PopupSettingsProvider } from './PopupSettingsProvider'; diff --git a/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx index 3a7242de7c..422065478c 100644 --- a/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx +++ b/packages/core/client/src/schema-component/antd/page/pagePopupUtils.tsx @@ -197,7 +197,7 @@ export const usePopupUtils = ( const getPopupContext = useCallback(() => { const context = { dataSource: dataSourceKey, - collection: association ? undefined : collection.name, + collection: association ? undefined : collection?.name, association, }; @@ -246,7 +246,7 @@ export const usePopupUtils = ( parentRecord: parentRecordData ? new CollectionRecord({ isNew: false, data: parentRecordData }) : parentRecord, service, dataSource: dataSourceKey, - collection: collection.name, + collection: collection?.name, association, sourceId, }); diff --git a/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts b/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts index a854eab15a..2c30024fd8 100644 --- a/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts +++ b/packages/core/client/src/schema-component/antd/page/usePopupContextInActionOrAssociationField.ts @@ -13,7 +13,7 @@ import { useCallback } from 'react'; import { useDesignable } from '../../hooks/useDesignable'; export interface PopupContext { - dataSource: string; + dataSource?: string; collection?: string; association?: string; } diff --git a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx index 2c7eca06c6..0efc2e9b0c 100644 --- a/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -47,7 +47,7 @@ export const canMakeAssociationBlock = (field) => { function useRecordBlocks() { const collection = useCollection(); const { getChildrenCollections } = useCollectionManager_deprecated(); - const collectionsWithView = getChildrenCollections(collection.name, true, collection.dataSource).filter( + const collectionsWithView = getChildrenCollections(collection?.name, true, collection?.dataSource).filter( (v) => v?.filterTargetKey, ); @@ -56,8 +56,8 @@ function useRecordBlocks() { name: 'details', title: '{{t("Details")}}', Component: 'DetailsBlockInitializer', - collectionName: collection.name, - dataSource: collection.dataSource, + collectionName: collection?.name, + dataSource: collection?.dataSource, useComponentProps() { const currentCollection = useCollection(); const { createSingleDetailsSchema, templateWrap } = useCreateSingleDetailsSchema(); @@ -122,8 +122,8 @@ function useRecordBlocks() { name: 'editForm', title: '{{t("Form (Edit)")}}', Component: 'FormBlockInitializer', - collectionName: collection.name, - dataSource: collection.dataSource, + collectionName: collection?.name, + dataSource: collection?.dataSource, useComponentProps() { const currentCollection = useCollection(); const { createEditFormBlock, templateWrap: templateWrapEdit } = useCreateEditFormBlock(); @@ -151,8 +151,8 @@ function useRecordBlocks() { name: 'createForm', title: '{{t("Form (Add new)")}}', Component: 'FormBlockInitializer', - collectionName: collection.name, - dataSource: collection.dataSource, + collectionName: collection?.name, + dataSource: collection?.dataSource, useComponentProps() { const { createAssociationFormBlock, templateWrap } = useCreateAssociationFormBlock(); const { createFormBlock, templateWrap: templateWrapCollection } = useCreateFormBlock(); diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts index a550810d5e..67f78b597a 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/hooks/useCustomizeRequestActionProps.ts @@ -65,7 +65,7 @@ export const useCustomizeRequestActionProps = () => { if (res.headers['content-disposition']) { const regex = /attachment;\s*filename="([^"]+)"/; const match = res.headers['content-disposition'].match(regex); - if (match[1]) { + if (match?.[1]) { saveAs(res.data, match[1]); } } diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts index 4f6686ea0e..baf19de2c2 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/schemaSettings.ts @@ -39,7 +39,7 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({ name: 'linkageRules', Component: SchemaSettingsLinkageRules, useComponentProps() { - const { name } = useCollection(); + const { name } = useCollection() || {}; const { linkageRulesProps } = useSchemaToolbar(); return { ...linkageRulesProps, @@ -48,7 +48,7 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({ }, useVisible() { const record = useCollectionRecord(); - return !record?.isNew; + return record && !record?.isNew; }, }, { @@ -75,6 +75,10 @@ export const customizeCustomRequestActionSettings = new SchemaSettings({ isPopupAction: false, }; }, + useVisible() { + const collection = useCollection(); + return !!collection; + }, }, { name: 'delete', diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx index cd9caec9aa..c708539cdf 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx @@ -8,10 +8,12 @@ */ import { useFieldSchema } from '@formily/react'; -import { Action, Icon, withDynamicSchemaProps } from '@nocobase/client'; +import { Action, Icon, useComponent, withDynamicSchemaProps } from '@nocobase/client'; import { Avatar } from 'antd'; import { createStyles } from 'antd-style'; -import React from 'react'; +import React, { useContext } from 'react'; +import { WorkbenchBlockContext } from './WorkbenchBlock'; +import { WorkbenchLayout } from './workbenchBlockSettings'; const useStyles = createStyles(({ token, css }) => ({ // 支持 css object 的写法 @@ -23,6 +25,8 @@ const useStyles = createStyles(({ token, css }) => ({ `, title: css` margin-top: ${token.marginSM}px; + text-overflow: ellipsis; + overflow: hidden; `, })); @@ -30,18 +34,30 @@ function Button() { const fieldSchema = useFieldSchema(); const icon = fieldSchema['x-component-props']?.['icon']; const backgroundColor = fieldSchema['x-component-props']?.['iconColor']; + const { layout } = useContext(WorkbenchBlockContext); const { styles, cx } = useStyles(); - - return ( -
+ return layout === WorkbenchLayout.Grid ? ( +
} />
{fieldSchema.title}
+ ) : ( + {fieldSchema.title} ); } export const WorkbenchAction = withDynamicSchemaProps((props) => { const { className, ...others } = props; const { styles, cx } = useStyles(); - return } />; + const fieldSchema = useFieldSchema(); + const Component = useComponent(props?.targetComponent) || Action; + return ( + } + confirmTitle={fieldSchema.title} + /> + ); }); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchBlock.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchBlock.tsx index 1be89fd6db..a058088a0d 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchBlock.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchBlock.tsx @@ -9,15 +9,18 @@ import { RecursionField, observer, useFieldSchema } from '@formily/react'; import { + CollectionContext, + DataSourceContext, DndContext, - SchemaComponentOptions, useDesignable, useSchemaInitializerRender, withDynamicSchemaProps, + Icon, } from '@nocobase/client'; -import { Space } from 'antd'; -import React from 'react'; -import { WorkbenchAction } from './WorkbenchAction'; +import { css, cx } from '@emotion/css'; +import { Space, List, Avatar } from 'antd'; +import React, { createContext } from 'react'; +import { WorkbenchLayout } from './workbenchBlockSettings'; const ConfigureActionsButton = observer( () => { @@ -31,26 +34,69 @@ const ConfigureActionsButton = observer( const InternalIcons = () => { const fieldSchema = useFieldSchema(); const { designable } = useDesignable(); - + const { layout = WorkbenchLayout.Grid } = fieldSchema.parent['x-component-props'] || {}; return (
- - {fieldSchema.mapProperties((s, key) => ( - - ))} - + {layout === WorkbenchLayout.Grid ? ( + + {fieldSchema.mapProperties((s, key) => ( + + ))} + + ) : ( + + {fieldSchema.mapProperties((s, key) => { + const icon = s['x-component-props']?.['icon']; + const backgroundColor = s['x-component-props']?.['iconColor']; + return ( + + } />} + title={} + > + + ); + })} + + )}
); }; +export const WorkbenchBlockContext = createContext({ layout: 'grid' }); + export const WorkbenchBlock: any = withDynamicSchemaProps( (props) => { + const fieldSchema = useFieldSchema(); + const { layout = 'grid' } = fieldSchema['x-component-props'] || {}; + return ( -
- {props.children} -
+ + + {props.children} + + ); }, { displayName: 'WorkbenchBlock' }, diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx new file mode 100644 index 0000000000..3c3ba21adb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.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. + */ + +import { ButtonEditor, SchemaSettings, SchemaSettingsActionLinkItem, useSchemaInitializer } from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; +export const workbenchActionSettingsCustomRequest = new SchemaSettings({ + name: 'workbench:actionSettings:customRequest', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + return { hasIconColor: true }; + }, + }, + { + name: 'editLink', + Component: SchemaSettingsActionLinkItem, + }, + { + sort: 800, + name: 'd1', + type: 'divider', + }, + { + sort: 900, + type: 'remove', + name: 'remove', + }, + ], +}); + +export function WorkbenchCustomRequestActionSchemaInitializerItem(props) { + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + return ( + { + insert({ + type: 'void', + title: values.title, + 'x-component': 'WorkbenchAction', + 'x-component-props': { + icon: values.icon, + iconColor: values.iconColor, + targetComponent: 'CustomRequestAction', + }, + 'x-action': 'customize:form:request', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:customRequest', + 'x-decorator': 'CustomRequestAction.Decorator', + 'x-action-settings': { + onSuccess: { + manualClose: false, + redirecting: false, + successMessage: '{{t("Request success")}}', + }, + }, + }); + }} + /> + ); +} diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx new file mode 100644 index 0000000000..4c0c19de8b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx @@ -0,0 +1,145 @@ +/** + * 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 { + ButtonEditor, + SchemaSettingOpenModeSchemaItems, + SchemaSettings, + useOpenModeContext, + useSchemaInitializer, +} from '@nocobase/client'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; + +export const workbenchActionSettingsPopup = new SchemaSettings({ + name: 'workbench:actionSettings:popup', + items: [ + { + name: 'editButton', + Component: ButtonEditor, + useComponentProps() { + return { hasIconColor: true }; + }, + }, + + { + name: 'openMode', + Component: SchemaSettingOpenModeSchemaItems, + useComponentProps() { + const { t } = useTranslation(); + const { hideOpenMode } = useOpenModeContext(); + return { + openSize: !hideOpenMode, + modeOptions: hideOpenMode && [ + { label: t('Drawer'), value: 'drawer' }, + { label: t('Page'), value: 'page' }, + ], + }; + }, + }, + { + sort: 800, + name: 'd1', + type: 'divider', + }, + { + sort: 900, + type: 'remove', + name: 'remove', + }, + ], +}); + +export function WorkbenchPopupActionSchemaInitializerItem(props) { + // 调用插入功能 + const { insert } = useSchemaInitializer(); + const { t } = useTranslation(); + const { hideOpenMode } = useOpenModeContext(); + + return ( + { + insert({ + type: 'void', + title: values.title, + 'x-action': 'customize:popup', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'workbench:actionSettings:popup', + 'x-component': 'WorkbenchAction', + 'x-component-props': { + icon: values.icon, + iconColor: values.iconColor, + refreshDataBlockRequest: false, + }, + properties: { + drawer: { + type: 'void', + title: values.title, + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + type: 'void', + title: '{{t("Details")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: {}, + }, + }, + }, + }, + }, + }, + }, + }, + }); + }} + /> + ); +} diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/index.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/index.tsx index 9b073fb7af..71aed6b693 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/index.tsx @@ -8,20 +8,29 @@ */ import { Plugin } from '@nocobase/client'; +import { QRCodeScanner } from './components/qrcode-scanner'; +import { WorkbenchAction } from './WorkbenchAction'; import { WorkbenchBlock } from './WorkbenchBlock'; +import { workbenchBlockInitializerItem } from './workbenchBlockInitializerItem'; +import { workbenchBlockSettings } from './workbenchBlockSettings'; +import { workbenchConfigureActions } from './workbenchConfigureActions'; import { workbenchActionSettingsLink } from './WorkbenchLinkActionSchemaInitializerItem'; +import { + WorkbenchPopupActionSchemaInitializerItem, + workbenchActionSettingsPopup, +} from './WorkbenchPopupActionSchemaInitializerItem'; import { WorkbenchScanActionSchemaInitializerItem, workbenchActionSettingsScanQrCode, } from './WorkbenchScanActionSchemaInitializerItem'; -import { QRCodeScanner } from './components/qrcode-scanner'; -import { workbenchBlockInitializerItem } from './workbenchBlockInitializerItem'; -import { workbenchBlockSettings } from './workbenchBlockSettings'; -import { workbenchConfigureActions } from './workbenchConfigureActions'; +import { + WorkbenchCustomRequestActionSchemaInitializerItem, + workbenchActionSettingsCustomRequest, +} from './WorkbenchCustomRequestActionSchemaInitializerItem'; export class PluginBlockWorkbenchClient extends Plugin { async load() { - this.app.addComponents({ WorkbenchBlock, QRCodeScanner }); + this.app.addComponents({ WorkbenchBlock, QRCodeScanner, WorkbenchAction }); // 新增工作台区块的设置器 this.app.schemaSettingsManager.add(workbenchBlockSettings); @@ -56,6 +65,17 @@ export class PluginBlockWorkbenchClient extends Plugin { this.app.schemaInitializerManager.addItem('workbench:configureActions', `qrcode`, { Component: WorkbenchScanActionSchemaInitializerItem, }); + + // 打开弹窗 + this.app.schemaSettingsManager.add(workbenchActionSettingsPopup); + this.app.schemaInitializerManager.addItem('workbench:configureActions', `popup`, { + Component: WorkbenchPopupActionSchemaInitializerItem, + }); + // 自定义请求 + this.app.schemaSettingsManager.add(workbenchActionSettingsCustomRequest); + this.app.schemaInitializerManager.addItem('workbench:configureActions', `customRequest`, { + Component: WorkbenchCustomRequestActionSchemaInitializerItem, + }); } } diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockInitializerItem.ts b/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockInitializerItem.ts index e48d9c5b17..4088616dab 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockInitializerItem.ts +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockInitializerItem.ts @@ -19,7 +19,7 @@ export const workbenchBlockInitializerItem: SchemaInitializerItemType = { const { t } = useTranslation('@nocobase/plugin-block-workbench'); const { insert } = useSchemaInitializer(); return { - title: t('Workbench'), + title: t('Action panel'), onClick: () => { insert(blockSchema); }, diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.ts b/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.ts deleted file mode 100644 index 0b14c91f49..0000000000 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 { SchemaSettings } from '@nocobase/client'; - -export const workbenchBlockSettings = new SchemaSettings({ - name: 'blockSettings:workbench', - items: [ - { - type: 'remove', - name: 'remove', - }, - ], -}); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.tsx new file mode 100644 index 0000000000..0b8ef4851d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/workbenchBlockSettings.tsx @@ -0,0 +1,62 @@ +/** + * 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 { SchemaSettings, SchemaSettingsSelectItem, useDesignable } from '@nocobase/client'; +import React from 'react'; +import { useField, useFieldSchema } from '@formily/react'; +import { useTranslation } from 'react-i18next'; +export const WorkbenchLayout = { + Grid: 'grid', + List: 'list', +}; + +const ActionPanelLayout = () => { + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field = useField(); + const { dn } = useDesignable(); + return ( + { + field.componentProps.layout = value; + const schema = { + 'x-uid': fieldSchema['x-uid'], + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + schema['x-component-props'] = fieldSchema['x-component-props'] || {}; + schema['x-component-props'].layout = value; + fieldSchema['x-component-props'].layout = value; + dn.emit('patch', { + schema: schema, + }); + dn.refresh(); + }} + /> + ); +}; + +export const workbenchBlockSettings = new SchemaSettings({ + name: 'blockSettings:workbench', + items: [ + { + name: 'layout', + Component: ActionPanelLayout, + }, + { + type: 'remove', + name: 'remove', + }, + ], +}); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-block-workbench/src/locale/zh-CN.json index 747862c61d..955506753e 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/locale/zh-CN.json @@ -1,10 +1,15 @@ { - "Workbench": "工作台", + "Action panel": "操作面板", "Scan QR code": "扫描二维码", "Album": "相册", "No camera device detected": "未检测到摄像头设备", "You have not granted permission to use the camera": "您未授权使用摄像头", "Unknown error": "未知错误", "The image size is too large. Please compress it to below 1MB before uploading": "图片尺寸过大,请压缩到1MB以下上传", - "QR code recognition failed, please scan again": "二维码识别失败,请重新扫描" -} \ No newline at end of file + "QR code recognition failed, please scan again": "二维码识别失败,请重新扫描", + "Layout": "布局", + "Grid": "栅格", + "List": "列表", + "Add popup": "添加弹窗", + "Add custom request":"添加自定义请求" +} diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx index 52157ee421..cec1190a64 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.tsx @@ -15,6 +15,7 @@ import { useProps, withDynamicSchemaProps, } from '@nocobase/client'; +import _ from 'lodash'; import React, { useMemo } from 'react'; import { MapBlockComponent } from '../components'; import { MapBlockDrawer } from '../components/MapBlockDrawer'; @@ -23,7 +24,7 @@ export const MapBlock = withDynamicSchemaProps((props) => { const { context } = usePopupUtils(); // only render the popup - if (context.currentLevel) { + if (context.currentLevel && !_.isNil(context.params?.filterbytk)) { return ( diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx index 3c3d474642..891871d14f 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/MobileActionPage.tsx @@ -11,83 +11,23 @@ import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { BackButtonUsedInSubPage, SchemaComponent, - SchemaInitializer, TabsContextProvider, useActionContext, - useApp, useTabsContext, } from '@nocobase/client'; -import _ from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo } from 'react'; import { createPortal } from 'react-dom'; -import { usePluginTranslation } from '../../locale'; import { BasicZIndexProvider, MIN_Z_INDEX_INCREMENT, useBasicZIndex } from '../BasicZIndexProvider'; import { useMobileActionPageStyle } from './MobileActionPage.style'; import { MobileTabsForMobileActionPage } from './MobileTabsForMobileActionPage'; const components = { Tabs: MobileTabsForMobileActionPage }; -/** - * 把 popup:common:addBlock 替换为移动端专属的值。当退出子页面时,再换回来。 - * - * 之所以要把这个过程放到子页面组件这里,是因为 dataBlocks 的 useChildren 必须要在子页面的上下文中运行。 - * - * @param supportsDataBlocks 支持在子页面中使用的数据区块 name - */ -const useMobileBlockInitializersInSubpage = ( - supportsDataBlocks = ['details', 'editForm', 'createForm', 'table', 'gridCard'], -) => { - const app = useApp(); - const [originalInitializers] = useState(() => - app.schemaInitializerManager.get('popup:common:addBlock'), - ); - const { t } = usePluginTranslation(); - const { visible } = useActionContext(); - - const dataBlocks = originalInitializers.options.items.find((item) => item.name === 'dataBlocks'); - const dataBlocksChildren = [...dataBlocks.useChildren(), ...dataBlocks.children]; - - const [newInitializers] = useState(() => { - const options = _.cloneDeep(originalInitializers.options); - options.items = options.items.filter((item) => { - if (item.name === 'dataBlocks') { - item.title = t('Desktop data blocks'); - item.children = dataBlocksChildren.filter((child) => { - return supportsDataBlocks.includes(child.name); - }); - item.useChildren = () => []; - return true; - } - - if (item.name === 'otherBlocks') { - item.title = t('Other desktop blocks'); - } - - return item.name !== 'filterBlocks'; - }); - - return new SchemaInitializer(options); - }); - - useEffect(() => { - return () => { - app.schemaInitializerManager.add(originalInitializers); - }; - }, [app, originalInitializers]); - - if (visible) { - // 把 PC 端子页面的 Add block 按钮换成移动端的。在退出移动端时,再换回来 - app.schemaInitializerManager.add(newInitializers); - } -}; - /** * 在移动端通过 Action 按钮打开的页面 * @returns */ export const MobileActionPage = ({ level, footerNodeName }) => { - useMobileBlockInitializersInSubpage(); - const field = useField(); const fieldSchema = useFieldSchema(); const ctx = useActionContext(); diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/blockInitializers.ts b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/blockInitializers.ts new file mode 100644 index 0000000000..b2ca74cafc --- /dev/null +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/adaptor-of-desktop/mobile-action-page/blockInitializers.ts @@ -0,0 +1,47 @@ +/** + * 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 { SchemaInitializer, useApp } from '@nocobase/client'; + +export const getMobilePopupBlockInitializers = (desktopPopupBlockInitializers: SchemaInitializer) => { + const dataBlocks = desktopPopupBlockInitializers.options.items.find((item) => item.name === 'dataBlocks'); + const otherBlocks = desktopPopupBlockInitializers.options.items.find((item) => item.name === 'otherBlocks'); + const keepItems = ['details', 'editForm', 'createForm', 'table', 'gridCard']; + + return new SchemaInitializer({ + ...desktopPopupBlockInitializers.options, + name: 'mobile:popup:common:addBlock', + items: [ + { + ...dataBlocks, + children: [], + useChildren: () => { + return dataBlocks.useChildren().filter((item) => keepItems.includes(item.name)); + }, + title: '{{t("Desktop data blocks")}}', + }, + { + ...otherBlocks, + title: '{{t("Other desktop blocks")}}', + }, + ], + }); +}; + +export const useToAddMobilePopupBlockInitializers = () => { + const app = useApp(); + const desktopPopupBlockInitializers = app.schemaInitializerManager.get('popup:common:addBlock'); + + if (!desktopPopupBlockInitializers) { + return; + } + + const mobilePopupBlockInitializers = getMobilePopupBlockInitializers(desktopPopupBlockInitializers); + app.schemaInitializerManager.add(mobilePopupBlockInitializers); +}; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx index 762da0a527..8a3f0164d7 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/mobile/Mobile.tsx @@ -25,6 +25,7 @@ import { ActionDrawerUsedInMobile, useToAdaptActionDrawerToMobile } from '../ada import { BasicZIndexProvider } from '../adaptor-of-desktop/BasicZIndexProvider'; import { useToAdaptFilterActionToMobile } from '../adaptor-of-desktop/FilterAction'; import { InternalPopoverNesterUsedInMobile } from '../adaptor-of-desktop/InternalPopoverNester'; +import { useToAddMobilePopupBlockInitializers } from '../adaptor-of-desktop/mobile-action-page/blockInitializers'; import { MobileActionPage } from '../adaptor-of-desktop/mobile-action-page/MobileActionPage'; import { ResetSchemaOptionsProvider } from '../adaptor-of-desktop/ResetSchemaOptionsProvider'; import { PageBackgroundColor } from '../constants'; @@ -36,6 +37,7 @@ import { useStyles } from './styles'; export const Mobile = () => { useToAdaptFilterActionToMobile(); useToAdaptActionDrawerToMobile(); + useToAddMobilePopupBlockInitializers(); const { styles } = useStyles(); const mobilePlugin = usePlugin(PluginMobileClient); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx index e49f458f03..bb5205ef97 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowManualProvider.tsx @@ -41,5 +41,5 @@ function cacheSchema(collectionNameList: string[]) { cacheSchema(Object.values(collections).map((collection) => collection.name)); function isMobile() { - return window.location.pathname.startsWith('/m'); + return window.location.pathname.startsWith('/m/'); } From bd942342b0c7cc2fd9808dd3e66bdd7dd4f0c84e Mon Sep 17 00:00:00 2001 From: YANG QIA <2013xile@gmail.com> Date: Fri, 6 Sep 2024 14:43:14 +0800 Subject: [PATCH 12/68] fix(auth): should set user token as invalid when changing password (#5212) * fix(auth): should log user out when changing password * fix: add passwordChangeTZ * fix: clear local token when token is invalid * fix: test * fix: field name --- .../auth/src/__tests__/middleware.test.ts | 2 +- packages/core/auth/src/auth-manager.ts | 5 +++- packages/core/auth/src/base/auth.ts | 11 +++++-- .../core/client/src/api-client/APIClient.ts | 5 +++- .../core/client/src/user/ChangePassword.tsx | 3 ++ packages/core/logger/src/request-logger.ts | 11 +++++-- packages/core/server/src/locale/locale.ts | 1 + packages/core/test/src/server/mock-server.ts | 1 + .../@nocobase/plugin-acl/src/server/server.ts | 2 +- .../src/server/__tests__/actions.test.ts | 24 +++++++++++++++ .../plugin-auth/src/server/plugin.ts | 12 ++------ .../src/server/collections/users.ts | 4 +++ .../plugin-users/src/server/server.ts | 29 ++++++++----------- 13 files changed, 74 insertions(+), 36 deletions(-) diff --git a/packages/core/auth/src/__tests__/middleware.test.ts b/packages/core/auth/src/__tests__/middleware.test.ts index 3f7333f07c..0f0bd09d50 100644 --- a/packages/core/auth/src/__tests__/middleware.test.ts +++ b/packages/core/auth/src/__tests__/middleware.test.ts @@ -71,7 +71,7 @@ describe('middleware', () => { hasFn.mockImplementation(() => true); const res = await agent.resource('auth').check(); expect(res.status).toBe(401); - expect(res.text).toContain('token is not available'); + expect(res.text).toContain('Token is invalid'); }); }); }); diff --git a/packages/core/auth/src/auth-manager.ts b/packages/core/auth/src/auth-manager.ts index f7bd79d899..f0c625a1e5 100644 --- a/packages/core/auth/src/auth-manager.ts +++ b/packages/core/auth/src/auth-manager.ts @@ -109,7 +109,10 @@ export class AuthManager { return async (ctx: Context & { auth: Auth }, next: Next) => { const token = ctx.getBearerToken(); if (token && (await ctx.app.authManager.jwt.blacklist?.has(token))) { - return ctx.throw(401, ctx.t('token is not available')); + return ctx.throw(401, { + code: 'TOKEN_INVALID', + message: ctx.t('Token is invalid'), + }); } const name = ctx.get(this.options.authKey) || this.options.default; diff --git a/packages/core/auth/src/base/auth.ts b/packages/core/auth/src/base/auth.ts index 4c8659f63c..bab49103fe 100644 --- a/packages/core/auth/src/base/auth.ts +++ b/packages/core/auth/src/base/auth.ts @@ -69,14 +69,14 @@ export class BaseAuth extends Auth { return null; } try { - const { userId, roleName } = await this.jwt.decode(token); + const { userId, roleName, iat, temp } = await this.jwt.decode(token); if (roleName) { this.ctx.headers['x-role'] = roleName; } const cache = this.ctx.cache as Cache; - return await cache.wrap(this.getCacheKey(userId), () => + const user = await cache.wrap(this.getCacheKey(userId), () => this.userRepository.findOne({ filter: { id: userId, @@ -84,6 +84,10 @@ export class BaseAuth extends Auth { raw: true, }), ); + if (temp && user.passwordChangeTz && iat * 1000 < user.passwordChangeTz) { + throw new Error('Token is invalid'); + } + return user; } catch (err) { this.ctx.logger.error(err, { method: 'check' }); return null; @@ -106,6 +110,7 @@ export class BaseAuth extends Auth { } const token = this.jwt.sign({ userId: user.id, + temp: true, }); return { user, @@ -119,7 +124,7 @@ export class BaseAuth extends Auth { return; } const { userId } = await this.jwt.decode(token); - await this.ctx.app.emitAsync('beforeSignOut', { userId }); + await this.ctx.app.emitAsync('cache:del:roles', { userId }); await this.ctx.cache.del(this.getCacheKey(userId)); return await this.jwt.block(token); } diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 5c6993aa3f..23d11e64f5 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -98,6 +98,9 @@ export class APIClient extends APIClientSDK { if (errs.find((error: { code?: string }) => error.code === 'ROLE_NOT_FOUND_ERR')) { this.auth.setRole(null); } + if (errs.find((error: { code?: string }) => error.code === 'TOKEN_INVALID')) { + this.auth.setToken(null); + } throw error; }, ); @@ -119,7 +122,7 @@ export class APIClient extends APIClientSDK { } return response; }, - (error) => { + async (error) => { if (this.silence) { throw error; } diff --git a/packages/core/client/src/user/ChangePassword.tsx b/packages/core/client/src/user/ChangePassword.tsx index 226d9d9eb2..c6837ff799 100644 --- a/packages/core/client/src/user/ChangePassword.tsx +++ b/packages/core/client/src/user/ChangePassword.tsx @@ -14,6 +14,7 @@ import React, { useContext, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActionContextProvider, DropdownVisibleContext, SchemaComponent, useActionContext } from '../'; import { useAPIClient } from '../api-client'; +import { useNavigate } from 'react-router-dom'; const useCloseAction = () => { const { setVisible } = useActionContext(); @@ -29,6 +30,7 @@ const useCloseAction = () => { }; const useSaveCurrentUserValues = () => { + const navigate = useNavigate(); const { setVisible } = useActionContext(); const form = useForm(); const api = useAPIClient(); @@ -40,6 +42,7 @@ const useSaveCurrentUserValues = () => { }); await form.reset(); setVisible(false); + navigate('/signin'); }, }; }; diff --git a/packages/core/logger/src/request-logger.ts b/packages/core/logger/src/request-logger.ts index 8875f4c53e..fc6e78a401 100644 --- a/packages/core/logger/src/request-logger.ts +++ b/packages/core/logger/src/request-logger.ts @@ -7,9 +7,8 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { getLoggerFilePath } from './config'; import { Logger, LoggerOptions } from './logger'; -import { pick } from 'lodash'; +import { pick, omit } from 'lodash'; const defaultRequestWhitelist = [ 'action', 'header.x-role', @@ -21,6 +20,12 @@ const defaultRequestWhitelist = [ 'referer', ]; const defaultResponseWhitelist = ['status']; +const defaultActionBlackList = [ + 'params.values.password', + 'params.values.confirmPassword', + 'params.values.oldPassword', + 'params.values.newPassword', +]; export interface RequestLoggerOptions extends LoggerOptions { skip?: (ctx?: any) => Promise; @@ -60,7 +65,7 @@ export const requestLogger = (appName: string, requestLogger: Logger, options?: message: `response ${ctx.url}`, ...requestInfo, res: pick(ctx.response.toJSON(), options?.responseWhitelist || defaultResponseWhitelist), - action: ctx.action?.toJSON?.(), + action: omit(ctx.action?.toJSON?.(), defaultActionBlackList), userId: ctx.auth?.user?.id, status: ctx.status, cost, diff --git a/packages/core/server/src/locale/locale.ts b/packages/core/server/src/locale/locale.ts index f04a5ecad3..4ec017eb00 100644 --- a/packages/core/server/src/locale/locale.ts +++ b/packages/core/server/src/locale/locale.ts @@ -45,6 +45,7 @@ export class Locale { name: 'locale', prefix: 'locale', store: 'memory', + max: 2000 }); await this.get(this.defaultLang); diff --git a/packages/core/test/src/server/mock-server.ts b/packages/core/test/src/server/mock-server.ts index 91dec44647..9f58cd5b45 100644 --- a/packages/core/test/src/server/mock-server.ts +++ b/packages/core/test/src/server/mock-server.ts @@ -123,6 +123,7 @@ export class MockServer extends Application { jwt.sign( { userId: typeof userOrId === 'number' ? userOrId : userOrId?.id, + temp: true, }, process.env.APP_KEY, { diff --git a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts index 37936e0a9e..73fcfe9302 100644 --- a/packages/plugins/@nocobase/plugin-acl/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-acl/src/server/server.ts @@ -408,7 +408,7 @@ export class PluginACLServer extends Plugin { }); }); - this.app.on('beforeSignOut', ({ userId }) => { + this.app.on('cache:del:roles', ({ userId }) => { this.app.cache.del(`roles:${userId}`); }); this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'auth' }); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts index c5f87ca261..27f3a96b25 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts @@ -102,6 +102,7 @@ describe('actions', () => { }); afterEach(async () => { + await app.db.clean({ drop: true }); await app.destroy(); }); @@ -273,5 +274,28 @@ describe('actions', () => { expect(res.statusCode).toEqual(400); expect(res.error.text).toBe('Please enter a password'); }); + + it('should sign user out when changing password', async () => { + const userRepo = db.getRepository('users'); + const user = await userRepo.create({ + values: { + username: 'test', + password: '12345', + }, + }); + const userAgent = await agent.login(user); + const res = await userAgent.post('/auth:check').set({ 'X-Authenticator': 'basic' }).send(); + expect(res.statusCode).toEqual(200); + expect(res.body.data.id).toBeDefined(); + const res2 = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({ + oldPassword: '12345', + newPassword: '123456', + confirmPassword: '123456', + }); + expect(res2.statusCode).toEqual(200); + const res3 = await userAgent.post('/auth:check').set({ 'X-Authenticator': 'basic' }).send(); + expect(res3.statusCode).toEqual(200); + expect(res3.body.data.id).toBeUndefined(); + }); }); }); diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts index 730d43b736..98e7c66175 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts @@ -29,15 +29,6 @@ export class PluginAuthServer extends Plugin { } async load() { - // Set up database - await this.importCollections(resolve(__dirname, 'collections')); - this.db.addMigrations({ - namespace: 'auth', - directory: resolve(__dirname, 'migrations'), - context: { - plugin: this, - }, - }); this.cache = await this.app.cacheManager.createCache({ name: 'auth', prefix: 'auth', @@ -85,6 +76,9 @@ export class PluginAuthServer extends Plugin { const cache = this.app.cache as Cache; await cache.del(`auth:${user.id}`); }); + this.app.on('cache:del:auth', async ({ userId }) => { + await this.cache.del(`auth:${userId}`); + }); } async install(options?: InstallOptions) { diff --git a/packages/plugins/@nocobase/plugin-users/src/server/collections/users.ts b/packages/plugins/@nocobase/plugin-users/src/server/collections/users.ts index 5eccf34a0c..fdfaf654cf 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/collections/users.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/collections/users.ts @@ -91,6 +91,10 @@ export default defineCollection({ 'x-component': 'Password', }, }, + { + name: 'passwordChangeTz', + type: 'bigInt', + }, { type: 'string', name: 'appLang', diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index 1034775186..b1c00d914b 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -7,11 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Collection, Op } from '@nocobase/database'; +import { Collection, Model, Op } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; import { parse } from '@nocobase/utils'; -import { resolve } from 'path'; -import { Cache } from '@nocobase/cache'; import * as actions from './actions/users'; import { UserModel } from './models/UserModel'; import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync'; @@ -155,19 +153,9 @@ export default class PluginUsersServer extends Plugin { } async load() { - await this.importCollections(resolve(__dirname, 'collections')); - this.db.addMigrations({ - namespace: 'users', - directory: resolve(__dirname, 'migrations'), - context: { - plugin: this, - }, - }); - - this.app.resourcer.use(async (ctx, next) => { + this.app.resourceManager.use(async (ctx, next) => { await next(); const { associatedName, resourceName, actionName, values } = ctx.action.params; - const cache = ctx.app.cache as Cache; if ( associatedName === 'roles' && resourceName === 'users' && @@ -175,9 +163,7 @@ export default class PluginUsersServer extends Plugin { values?.length ) { // Delete cache when the members of a role changed - for (const userId of values) { - await cache.del(`roles:${userId}`); - } + await Promise.all(values.map((userId: number) => this.app.emitAsync('cache:del:roles', { userId }))); } }); @@ -185,6 +171,15 @@ export default class PluginUsersServer extends Plugin { if (userDataSyncPlugin && userDataSyncPlugin.enabled) { userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger)); } + + this.app.db.on('users.beforeUpdate', async (model: Model) => { + if (!model._changed.has('password')) { + return; + } + model.set('passwordChangeTz', Date.now()); + await this.app.emitAsync('cache:del:roles', { userId: model.get('id') }); + await this.app.emitAsync('cache:del:auth', { userId: model.get('id') }); + }); } getInstallingData(options: any = {}) { From 685bedd38e5c00d1356acddac876441c8da687a7 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Mon, 9 Sep 2024 10:21:10 +0800 Subject: [PATCH 13/68] chore: update ci --- .github/workflows/manual-build-pro-plugin-image.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/manual-build-pro-plugin-image.yml b/.github/workflows/manual-build-pro-plugin-image.yml index 2ca342f115..ef7b8b99b7 100644 --- a/.github/workflows/manual-build-pro-plugin-image.yml +++ b/.github/workflows/manual-build-pro-plugin-image.yml @@ -37,6 +37,8 @@ jobs: - name: Checkout plugin uses: actions/checkout@v3 with: + ref: main + token: ${{ steps.app-token.outputs.token }} repository: nocobase/plugin-${{ inputs.pro_plugin }} path: packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }} - name: Checkout pr From 0840024ba031dae84223fd9f8c78fb12c15c5849 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Mon, 9 Sep 2024 11:36:22 +0800 Subject: [PATCH 14/68] chore: update ci --- .github/workflows/manual-build-pro-plugin-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/manual-build-pro-plugin-image.yml b/.github/workflows/manual-build-pro-plugin-image.yml index ef7b8b99b7..6d674739e4 100644 --- a/.github/workflows/manual-build-pro-plugin-image.yml +++ b/.github/workflows/manual-build-pro-plugin-image.yml @@ -46,6 +46,7 @@ jobs: run: | cd ./packages/pro-plugins/@nocobase/plugin-${{ inputs.pro_plugin }} gh pr checkout ${{ inputs.pr_number }} + cd ../../../../ env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: rm .git From 8cf53cb7e60eaa7acb737d4b3cf77bef083856a4 Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Mon, 9 Sep 2024 14:18:42 +0800 Subject: [PATCH 15/68] chore: update ci --- .github/workflows/manual-build-pro-plugin-image.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/manual-build-pro-plugin-image.yml b/.github/workflows/manual-build-pro-plugin-image.yml index 6d674739e4..07857c7138 100644 --- a/.github/workflows/manual-build-pro-plugin-image.yml +++ b/.github/workflows/manual-build-pro-plugin-image.yml @@ -57,6 +57,17 @@ jobs: uses: docker/setup-buildx-action@v2 with: driver-opts: network=host + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + nocobase/nocobase + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} - name: Login to Aliyun Container Registry uses: docker/login-action@v2 with: From f97e2f42fbe7bac5b0c3757644643f33a464f27c Mon Sep 17 00:00:00 2001 From: xilesun <2013xile@gmail.com> Date: Mon, 9 Sep 2024 14:41:45 +0800 Subject: [PATCH 16/68] chore: update ci --- .github/workflows/manual-build-pro-plugin-image.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/manual-build-pro-plugin-image.yml b/.github/workflows/manual-build-pro-plugin-image.yml index 07857c7138..7990add6ff 100644 --- a/.github/workflows/manual-build-pro-plugin-image.yml +++ b/.github/workflows/manual-build-pro-plugin-image.yml @@ -57,17 +57,6 @@ jobs: uses: docker/setup-buildx-action@v2 with: driver-opts: network=host - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - nocobase/nocobase - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - name: Login to Aliyun Container Registry uses: docker/login-action@v2 with: @@ -77,7 +66,7 @@ jobs: - name: Set tags id: set-tags run: | - echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/pr-${{ inputs.pr_number }}-${{ inputs.pro_plugin }}" + echo "::set-output name=tags::${{ secrets.ALI_DOCKER_REGISTRY }}/nocobase/nocobase:pr-${{ inputs.pr_number }}-${{ inputs.pro_plugin }}" - name: IMAGE_TAG env: IMAGE_TAG: pr-${{ inputs.pr_number }} From 2180c7aad844a7502d9db84b1dc70dce198be4ed Mon Sep 17 00:00:00 2001 From: Katherine Date: Mon, 9 Sep 2024 19:36:49 +0800 Subject: [PATCH 17/68] refactor:action panel for mobile to support mobile:addBlock (#5235) * refactor: action panel for mobile to support mobile:addBlock * fix: test --- .../hooks/useSchemaInitializerRender.tsx | 5 +++-- .../client/src/modules/popup/OpenModeProvider.tsx | 13 +++++++++++-- .../WorkbenchPopupActionSchemaInitializerItem.tsx | 6 +++--- .../plugin-mobile/src/client/mobile/Mobile.tsx | 1 + 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx index b05a170aa0..ea5bf8af72 100644 --- a/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx +++ b/packages/core/client/src/application/schema-initializer/hooks/useSchemaInitializerRender.tsx @@ -15,7 +15,7 @@ import { SchemaInitializerButton } from '../components/SchemaInitializerButton'; import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializerOptions } from '../types'; import { withInitializer } from '../withInitializer'; - +import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; const InitializerComponent: FC> = React.memo((options) => { const Component: any = options.Component || SchemaInitializerButton; @@ -38,13 +38,14 @@ export function useSchemaInitializerRender( options?: Omit, 'name'>, ) { const app = useApp(); + const { isMobile } = useOpenModeContext() || {}; // compatible with mobile // TODO: delete this code if ( name === 'popup:common:addBlock' && app.schemaInitializerManager.has('mobile:popup:common:addBlock') && - window.location.pathname.startsWith('/m/') + isMobile ) { name = 'mobile:popup:common:addBlock'; } diff --git a/packages/core/client/src/modules/popup/OpenModeProvider.tsx b/packages/core/client/src/modules/popup/OpenModeProvider.tsx index a3d102e8fe..8a32996563 100644 --- a/packages/core/client/src/modules/popup/OpenModeProvider.tsx +++ b/packages/core/client/src/modules/popup/OpenModeProvider.tsx @@ -30,6 +30,11 @@ interface OpenModeProviderProps { * 隐藏 open mode 的配置选项 */ hideOpenMode?: boolean; + /** + * @default false + * 是否为Mobile路由下 + */ + isMobile?: boolean; } const defaultContext: OpenModeProviderProps = { @@ -46,6 +51,7 @@ const OpenModeContext = React.createContext<{ defaultOpenMode: OpenModeProviderProps['defaultOpenMode']; hideOpenMode: boolean; getComponentByOpenMode: (openMode: OpenMode) => any; + isMobile: boolean; }>(null); /** @@ -66,7 +72,9 @@ export const OpenModeProvider: FC = (props) => { if (props.hideOpenMode !== undefined) { result.hideOpenMode = props.hideOpenMode; } - + if (props.isMobile) { + result.isMobile = props.isMobile; + } return result; }, [props.defaultOpenMode, props.openModeToComponent, props.hideOpenMode]); @@ -88,8 +96,9 @@ export const OpenModeProvider: FC = (props) => { defaultOpenMode: context.defaultOpenMode, hideOpenMode: context.hideOpenMode, getComponentByOpenMode, + isMobile: context.isMobile, }; - }, [context.defaultOpenMode, context.hideOpenMode, getComponentByOpenMode]); + }, [context.defaultOpenMode, context.hideOpenMode, getComponentByOpenMode, context.isMobile]); return {props.children}; }; diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx index 4c0c19de8b..0894db145a 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx @@ -11,8 +11,8 @@ import { ButtonEditor, SchemaSettingOpenModeSchemaItems, SchemaSettings, - useOpenModeContext, useSchemaInitializer, + useOpenModeContext, } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -61,7 +61,7 @@ export function WorkbenchPopupActionSchemaInitializerItem(props) { // 调用插入功能 const { insert } = useSchemaInitializer(); const { t } = useTranslation(); - const { hideOpenMode } = useOpenModeContext(); + const { isMobile } = useOpenModeContext(); return ( { Date: Tue, 10 Sep 2024 07:49:54 +0900 Subject: [PATCH 18/68] feat(plugin-workflow-request): add xml and text to content type select (#5238) --- .../src/client/RequestInstruction.tsx | 45 ++++++++++++++++++- .../src/server/RequestInstruction.ts | 3 +- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-workflow-request/src/client/RequestInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-request/src/client/RequestInstruction.tsx index 8f6dd30b24..c74b76959e 100644 --- a/packages/plugins/@nocobase/plugin-workflow-request/src/client/RequestInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-request/src/client/RequestInstruction.tsx @@ -15,6 +15,7 @@ import { ArrayItems } from '@formily/antd-v5'; import { Instruction, WorkflowVariableJSON, + WorkflowVariableRawTextArea, WorkflowVariableTextArea, defaultFieldNames, } from '@nocobase/plugin-workflow/client'; @@ -91,6 +92,45 @@ const BodySchema = { }, }, }, + 'application/xml': { + type: 'void', + properties: { + data: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'WorkflowVariableRawTextArea', + 'x-component-props': { + placeholder: '', + autoSize: { + minRows: 10, + }, + className: css` + font-size: 80%; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + `, + }, + }, + }, + }, + 'text/plain': { + type: 'void', + properties: { + data: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'WorkflowVariableRawTextArea', + 'x-component-props': { + autoSize: { + minRows: 10, + }, + className: css` + font-size: 80%; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + `, + }, + }, + }, + }, }; function BodyComponent(props) { @@ -158,6 +198,8 @@ export default class extends Instruction { enum: [ { label: 'application/json', value: 'application/json' }, { label: 'application/x-www-form-urlencoded', value: 'application/x-www-form-urlencoded' }, + { label: 'application/xml', value: 'application/xml' }, + { label: 'text/plain', value: 'text/plain' }, ], default: 'application/json', }, @@ -316,8 +358,9 @@ export default class extends Instruction { components = { ArrayItems, BodyComponent, - WorkflowVariableTextArea, WorkflowVariableJSON, + WorkflowVariableTextArea, + WorkflowVariableRawTextArea, }; useVariables({ key, title, config }, { types }) { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts index 95e0bf0ea6..10c45cbce7 100644 --- a/packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts +++ b/packages/plugins/@nocobase/plugin-workflow-request/src/server/RequestInstruction.ts @@ -52,6 +52,7 @@ async function request(config) { // TODO(feat): only support JSON type for now, should support others in future headers['Content-Type'] = contentType; + const transformer = ContentTypeTransformers[contentType]; return axios.request({ url: trim(url), @@ -61,7 +62,7 @@ async function request(config) { timeout, ...(method.toLowerCase() !== 'get' && data != null ? { - data: ContentTypeTransformers[contentType](data), + data: transformer ? transformer(data) : data.toString(), } : {}), }); From a7df0e3fd32f27e86e883216d2d99bef85b3bd75 Mon Sep 17 00:00:00 2001 From: ChengLei Shao Date: Tue, 10 Sep 2024 15:25:20 +0800 Subject: [PATCH 19/68] refactor: datetime field (#5084) * refactor: date field support timezone, defaultToCurrentTime, and onUpdateToCurrentTime * refactor: availableTypes unixTimestamp * chore: defaultToCurrentTime * chore: unix timestamp field * fix: bug * chore: field type map * refactor: local improve * fix: bug * fix: bug * chore: timezone test * chore: test * fix: test * fix: test * chore: field setter * chore: test * chore: date only field * chore: test * chore: test * fix: bug * fix: unixTimestamp * fix: unixTimestamp * chore: accuracy * fix: bug * fix: bug * fix: unixTimestamp * fix: unixTimestamp * fix: date & datetime * refactor: add DateFieldInterface * fix: bug * chore: test * chore: test * chore: test * refactor: locale improve * refactor: local improve * fix: bug * refactor: unixTimestamp not support default value * refactor: timezone * refactor: datetimeNoTzFieldInterface * refactor: locale improve * refactor: locale improve * fix: test * fix: bug * chore: datetime no tz field * refactor: datetimeNoTz * refactor: datetime * fix: bug * refactor: timeFormat * style: collection fields style improve * refactor: defaultToCurrentTime * fix: datetime no tz * chore: field type map * fix: bug * fix: bug * refactor: createAt & updateAt * fix: bug * fix: no tz field with timezone * refactor: dateonly * fix: test * chore: data type map * fix: dateonly * fix: dateonly * fix: datetime * refactor: locale improve * refactor: unixtimestamp * fix: merge bug * fix: bug * fix: datetime * fix: datetime no tz * fix: datetime no tz * chore: mysql datetime map * chore: test * chore: test * chore: test * chore: datetimeTz field * fix: no interface option * refactor: update type * refactor: update type * fix: pg no tz field * chore: save iso8601 format to no tz field * fix: test * fix: test * refactor: gannt & calender startTime & endTime * refactor: unixTimestamp * chore: filter of datetime field * chore: test * chore: test * fix: test * fix: datetime no tz filter * chore: test * chore: test * fix: datetime default value in mysql * fix: sqlite test * chore: test * fix: test * fix: test * fix: $dateOn * fix: bug * fix: bug * refactor: datepicker * fix: test * refactor: datePicker * refactor: gantt setting --------- Co-authored-by: katherinehhh --- .../collection-manager/collectionPlugin.ts | 4 + .../interfaces/createdAt.ts | 2 +- .../collection-manager/interfaces/dateOnly.ts | 60 +++++ .../collection-manager/interfaces/datetime.ts | 20 +- .../interfaces/datetimeNoTz.ts | 65 +++++ .../collection-manager/interfaces/index.ts | 2 + .../interfaces/properties/index.ts | 9 +- .../src/collection-manager/interfaces/time.ts | 2 +- .../interfaces/unixTimestamp.tsx | 32 ++- .../interfaces/updatedAt.ts | 2 +- .../CollectionFieldInterface.ts | 4 +- packages/core/client/src/locale/zh-CN.json | 11 +- .../antd/date-picker/__tests__/util.test.ts | 4 +- .../schema-component/antd/date-picker/util.ts | 9 +- .../antd/filter/DynamicComponent.tsx | 1 + .../antd/unix-timestamp/UnixTimestamp.tsx | 41 +-- .../__tests__/UnixTimestamp.test.tsx | 78 +----- .../client/src/schema-initializer/utils.ts | 4 +- .../SchemaSettingsDateFormat.tsx | 6 +- .../src/__tests__/fields/date-only.test.ts | 42 +++ .../src/__tests__/fields/date.test.ts | 83 ------ .../__tests__/fields/datetime-no-tz.test.ts | 145 ++++++++++ .../src/__tests__/fields/datetime-tz.test.ts | 250 ++++++++++++++++++ .../fields/unix-timestamp-field.tests.ts | 86 ++++++ .../__tests__/operator/date/date-only.test.ts | 164 ++++++++++++ .../operator/date/datetime-no-tz.test.ts | 201 ++++++++++++++ .../datetime-tz.test.ts} | 6 +- .../operator/date/unix-timestamp.test.ts | 75 ++++++ .../src/__tests__/view/view-inference.test.ts | 6 +- packages/core/database/src/database.ts | 8 +- .../core/database/src/fields/date-field.ts | 105 +++++++- .../database/src/fields/date-only-field.ts | 21 ++ .../database/src/fields/datetime-field.ts | 17 ++ .../src/fields/datetime-no-tz-field.ts | 112 ++++++++ .../database/src/fields/datetime-tz-field.ts | 17 ++ packages/core/database/src/fields/field.ts | 8 +- packages/core/database/src/fields/index.ts | 13 + .../src/fields/unix-timestamp-field.ts | 84 ++++++ packages/core/database/src/model.ts | 17 +- packages/core/database/src/operators/date.ts | 94 +++++-- packages/core/database/src/repository.ts | 4 +- .../core/database/src/view/field-type-map.ts | 34 +-- packages/core/server/src/application.ts | 21 +- packages/core/utils/src/parse-date.ts | 5 + packages/core/utils/src/parse-filter.ts | 6 + .../src/client/calendar/Calender.Settings.tsx | 2 +- .../items/CalendarBlockInitializer.tsx | 2 +- .../collection-template/general3.test.ts | 5 +- .../src/client/__e2e__/utils.ts | 5 +- .../server/__tests__/fields/datetime.test.ts | 51 ++++ .../CollectionsManager/SetFilterTargetKey.tsx | 2 +- .../components/FieldType.tsx | 3 +- .../Configuration/CollectionFields.tsx | 4 +- .../src/client/Gantt.Settings.tsx | 4 +- .../src/client/GanttBlockInitializer.tsx | 2 +- .../plugin-gantt/src/client/utils.tsx | 10 +- .../client/__e2e__/customFormBlocks1.test.ts | 2 +- 57 files changed, 1773 insertions(+), 299 deletions(-) create mode 100644 packages/core/client/src/collection-manager/interfaces/dateOnly.ts create mode 100644 packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts create mode 100644 packages/core/database/src/__tests__/fields/date-only.test.ts delete mode 100644 packages/core/database/src/__tests__/fields/date.test.ts create mode 100644 packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts create mode 100644 packages/core/database/src/__tests__/fields/datetime-tz.test.ts create mode 100644 packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts create mode 100644 packages/core/database/src/__tests__/operator/date/date-only.test.ts create mode 100644 packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts rename packages/core/database/src/__tests__/operator/{date-operator.test.ts => date/datetime-tz.test.ts} (98%) create mode 100644 packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts create mode 100644 packages/core/database/src/fields/date-only-field.ts create mode 100644 packages/core/database/src/fields/datetime-field.ts create mode 100644 packages/core/database/src/fields/datetime-no-tz-field.ts create mode 100644 packages/core/database/src/fields/datetime-tz-field.ts create mode 100644 packages/core/database/src/fields/unix-timestamp-field.ts create mode 100644 packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts diff --git a/packages/core/client/src/collection-manager/collectionPlugin.ts b/packages/core/client/src/collection-manager/collectionPlugin.ts index 8d71266aaa..1082961953 100644 --- a/packages/core/client/src/collection-manager/collectionPlugin.ts +++ b/packages/core/client/src/collection-manager/collectionPlugin.ts @@ -52,6 +52,8 @@ import { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, } from './interfaces'; import { GeneralCollectionTemplate, @@ -173,6 +175,8 @@ export class CollectionPlugin extends Plugin { UUIDFieldInterface, NanoidFieldInterface, UnixTimestampFieldInterface, + DateFieldInterface, + DatetimeNoTzFieldInterface, ]); } diff --git a/packages/core/client/src/collection-manager/interfaces/createdAt.ts b/packages/core/client/src/collection-manager/interfaces/createdAt.ts index 97c0a6da24..c6c18713a8 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdAt.ts @@ -28,7 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts new file mode 100644 index 0000000000..defeb80b16 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts @@ -0,0 +1,60 @@ +/** + * 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 { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DateFieldInterface extends CollectionFieldInterface { + name = 'date'; + type = 'object'; + group = 'datetime'; + order = 3; + title = '{{t("DateOnly")}}'; + sortable = true; + default = { + type: 'dateOnly', + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + dateOnly: true, + }, + }, + }; + availableTypes = ['dateOnly']; + hasDefaultValue = true; + properties = { + ...defaultProps, + 'uiSchema.x-component-props.dateFormat': { + type: 'string', + title: '{{t("Date format")}}', + 'x-component': 'Radio.Group', + 'x-decorator': 'FormItem', + default: 'YYYY-MM-DD', + enum: [ + { + label: '{{t("Year/Month/Day")}}', + value: 'YYYY/MM/DD', + }, + { + label: '{{t("Year-Month-Day")}}', + value: 'YYYY-MM-DD', + }, + { + label: '{{t("Day/Month/Year")}}', + value: 'DD/MM/YYYY', + }, + ], + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/datetime.ts b/packages/core/client/src/collection-manager/interfaces/datetime.ts index 615d11e9ea..85d6b13c45 100644 --- a/packages/core/client/src/collection-manager/interfaces/datetime.ts +++ b/packages/core/client/src/collection-manager/interfaces/datetime.ts @@ -15,23 +15,39 @@ export class DatetimeFieldInterface extends CollectionFieldInterface { type = 'object'; group = 'datetime'; order = 1; - title = '{{t("Datetime")}}'; + title = '{{t("Datetime(with time zone)")}}'; sortable = true; default = { type: 'date', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: true, uiSchema: { type: 'string', 'x-component': 'DatePicker', 'x-component-props': { showTime: false, + utc: true, }, }, }; - availableTypes = ['date', 'dateOnly', 'string']; + availableTypes = ['date', 'string', 'datetime', 'datetimeTz']; hasDefaultValue = true; properties = { ...defaultProps, ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, 'uiSchema.x-component-props.gmt': { type: 'boolean', title: '{{t("GMT")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts new file mode 100644 index 0000000000..4cebfad159 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/datetimeNoTz.ts @@ -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 { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; +import { dateTimeProps, defaultProps, operators } from './properties'; + +export class DatetimeNoTzFieldInterface extends CollectionFieldInterface { + name = 'datetimeNoTz'; + type = 'object'; + group = 'datetime'; + order = 2; + title = '{{t("Datetime(without time zone)")}}'; + sortable = true; + default = { + type: 'datetimeNoTz', + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, + timezone: false, + uiSchema: { + type: 'string', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: false, + utc: false, + }, + }, + }; + availableTypes = ['string', 'datetimeNoTz']; + hasDefaultValue = true; + properties = { + ...defaultProps, + ...dateTimeProps, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current server time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp to the current server time on update")}}', + }, + 'uiSchema.x-component-props.gmt': { + type: 'boolean', + title: '{{t("GMT")}}', + 'x-hidden': true, + 'x-component': 'Checkbox', + 'x-content': '{{t("Use the same time zone (GMT) for all users")}}', + 'x-decorator': 'FormItem', + default: false, + }, + }; + filterable = { + operators: operators.datetime, + }; + titleUsable = true; +} diff --git a/packages/core/client/src/collection-manager/interfaces/index.ts b/packages/core/client/src/collection-manager/interfaces/index.ts index 6778d83413..e2a1dfd747 100644 --- a/packages/core/client/src/collection-manager/interfaces/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/index.ts @@ -46,3 +46,5 @@ export * from './sort'; export * from './uuid'; export * from './nanoid'; export * from './unixTimestamp'; +export * from './dateOnly'; +export * from './datetimeNoTz'; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 1ef36e4b61..edb01ef255 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -253,10 +253,11 @@ export const dateTimeProps: { [key: string]: ISchema } = { 'x-content': '{{t("Show time")}}', 'x-reactions': [ `{{(field) => { - field.query('..[].timeFormat').take(f => { - f.display = field.value ? 'visible' : 'none'; - }); - }}}`, + field.query('..[].timeFormat').take(f => { + f.display = field.value ? 'visible' : 'none'; + f.value='HH:mm:ss' + }); + }}}`, ], }, 'uiSchema.x-component-props.timeFormat': { diff --git a/packages/core/client/src/collection-manager/interfaces/time.ts b/packages/core/client/src/collection-manager/interfaces/time.ts index dc47c41746..2432a10bcb 100644 --- a/packages/core/client/src/collection-manager/interfaces/time.ts +++ b/packages/core/client/src/collection-manager/interfaces/time.ts @@ -14,7 +14,7 @@ export class TimeFieldInterface extends CollectionFieldInterface { name = 'time'; type = 'object'; group = 'datetime'; - order = 2; + order = 4; title = '{{t("Time")}}'; sortable = true; default = { diff --git a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx index 47b3ebc2cf..5b0de8c91a 100644 --- a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx +++ b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx @@ -8,31 +8,33 @@ */ import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { dateTimeProps, defaultProps, operators } from './properties'; - +import { defaultProps, operators } from './properties'; export class UnixTimestampFieldInterface extends CollectionFieldInterface { name = 'unixTimestamp'; type = 'object'; group = 'datetime'; - order = 1; + order = 4; title = '{{t("Unix Timestamp")}}'; sortable = true; default = { - type: 'bigInt', + type: 'unixTimestamp', + accuracy: 'second', + timezone: true, + defaultToCurrentTime: false, + onUpdateToCurrentTime: false, uiSchema: { type: 'number', 'x-component': 'UnixTimestamp', 'x-component-props': { - accuracy: 'second', showTime: true, }, }, }; - availableTypes = ['integer', 'bigInt']; - hasDefaultValue = true; + availableTypes = ['unixTimestamp']; + hasDefaultValue = false; properties = { ...defaultProps, - 'uiSchema.x-component-props.accuracy': { + accuracy: { type: 'string', title: '{{t("Accuracy")}}', 'x-component': 'Radio.Group', @@ -43,9 +45,21 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface { { value: 'second', label: '{{t("Second")}}' }, ], }, + defaultToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Default value to current time")}}', + }, + onUpdateToCurrentTime: { + type: 'boolean', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': '{{t("Automatically update timestamp on update")}}', + }, }; filterable = { - operators: operators.number, + operators: operators.datetime, }; titleUsable = true; } diff --git a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts index ad969338e7..533c008d7f 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts @@ -28,7 +28,7 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; - availableTypes = ['date']; + availableTypes = []; properties = { ...defaultProps, ...dateTimeProps, diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index 16cb0c008c..a0c616c7e3 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -119,8 +119,8 @@ export abstract class CollectionFieldInterface { }, }, { - dependencies: ['primaryKey', 'unique', 'autoIncrement'], - when: '{{$deps[0]||$deps[1]||$deps[2]}}', + dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'], + when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}', fulfill: { state: { hidden: true, diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 1771db3f99..6d09e9f9f3 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -283,7 +283,7 @@ "Checkbox group": "复选框", "China region": "中国行政区", "Date & Time": "日期 & 时间", - "Datetime": "日期", + "Datetime": "日期时间", "Relation": "关系类型", "Link to": "关联", "Link to description": "用于快速创建表关系,可兼容大多数普通场景。适合非开发人员使用。作为字段存在时,它是一个下拉选择用于选择目标数据表的数据。创建后,将同时在目标数据表中生成当前数据表的关联字段。", @@ -974,5 +974,12 @@ "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "在分页时跳过获取表记录总数,以加快加载速度,建议对有大量数据的数据表开启此选项", "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。", "Plugin dependency version mismatch": "插件依赖版本不一致", - "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?" + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?", + "Default value to current time": "设置字段默认值为当前时间", + "Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间", + "Default value to current server time": "设置字段默认值为当前服务端时间", + "Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间", + "Datetime(with time zone)": "日期时间(含时区)", + "Datetime(without time zone)": "日期时间(不含时区)", + "DateOnly":"仅日期" } diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts index a77fbe8183..5ef7d817f5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -102,7 +102,7 @@ describe('moment2str', () => { test('picker is year', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'year' }); + const str = moment2str(m, { picker: 'year', gmt: true }); expect(str).toBe('2023-01-01T00:00:00.000Z'); }); @@ -132,7 +132,7 @@ describe('moment2str', () => { test('picker is month', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'month' }); + const str = moment2str(m, { picker: 'month', gmt: true }); expect(str).toBe('2023-06-01T00:00:00.000Z'); }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 59026b3efb..026778aec5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -78,17 +78,20 @@ export const mapDatePicker = function () { return (props: any) => { const format = getDefaultFormat(props) as any; const onChange = props.onChange; - return { ...props, format: format, value: str2moment(props.value, props), - onChange: (value: Dayjs | null) => { + onChange: (value: Dayjs | null, dateString) => { if (onChange) { if (!props.showTime && value) { value = value.startOf('day'); } - onChange(moment2str(value, props)); + if (props.dateOnly) { + onChange(dateString !== '' ? dateString : undefined); + } else { + onChange(moment2str(value, props)); + } } }, }; diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 36987f612a..2c5062344e 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -64,6 +64,7 @@ export const DynamicComponent = (props: Props) => { minWidth: 150, ...props.style, }, + utc: false, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx index 5d19a1e118..e83f9d00bc 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx @@ -8,57 +8,32 @@ */ import { connect, mapReadPretty } from '@formily/react'; -import React, { useMemo } from 'react'; +import React from 'react'; import { DatePicker } from '../date-picker'; -import dayjs from 'dayjs'; - -const toValue = (value: any, accuracy) => { - if (value) { - return timestampToDate(value, accuracy); - } - return null; -}; - -function timestampToDate(timestamp, accuracy = 'millisecond') { - if (accuracy === 'second') { - timestamp *= 1000; // 如果精确度是秒级,则将时间戳乘以1000转换为毫秒级 - } - return dayjs(timestamp); -} - -function getTimestamp(date, accuracy = 'millisecond') { - if (accuracy === 'second') { - return dayjs(date).unix(); - } else { - return dayjs(date).valueOf(); // 默认返回毫秒级时间戳 - } -} interface UnixTimestampProps { - value?: number; - accuracy?: 'millisecond' | 'second'; + value?: any; onChange?: (value: number) => void; } export const UnixTimestamp = connect( (props: UnixTimestampProps) => { - const { value, onChange, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); + const { value, onChange } = props; + return ( { if (onChange) { - onChange(getTimestamp(v, accuracy)); + onChange(v); } }} /> ); }, mapReadPretty((props) => { - const { value, accuracy = 'second' } = props; - const v = useMemo(() => toValue(value, accuracy), [value, accuracy]); - return ; + const { value } = props; + return ; }), ); diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx index ba82c73147..c9c107f45c 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/__tests__/UnixTimestamp.test.tsx @@ -13,11 +13,9 @@ import { UnixTimestamp } from '@nocobase/client'; describe('UnixTimestamp', () => { it('renders without errors', async () => { const { container } = await renderAppOptions({ - Component: UnixTimestamp, - props: { - accuracy: 'millisecond', - }, - value: 0, + Component: UnixTimestamp as any, + props: {}, + value: null, }); expect(container).toMatchInlineSnapshot(`
@@ -69,78 +67,10 @@ describe('UnixTimestamp', () => { `); }); - it('millisecond', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630000, - props: { - accuracy: 'millisecond', - }, - }); - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('second', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: 1712819630, - props: { - accuracy: 'second', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('string', async () => { - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, - }); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-11'); - }); - }); - - it('change', async () => { - const onChange = vitest.fn(); - await renderAppOptions({ - Component: UnixTimestamp, - value: '2024-04-11', - onChange, - props: { - accuracy: 'millisecond', - }, - }); - await userEvent.click(screen.getByRole('textbox')); - - await waitFor(() => { - expect(screen.queryByRole('table')).toBeInTheDocument(); - }); - - await userEvent.click(document.querySelector('td[title="2024-04-12"]')); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('2024-04-12'); - }); - expect(onChange).toBeCalledWith(1712880000000); - }); - it('read pretty', async () => { const { container } = await renderReadPrettyApp({ - Component: UnixTimestamp, + Component: UnixTimestamp as any, value: '2024-04-11', - props: { - accuracy: 'millisecond', - }, }); expect(screen.getByText('2024-04-11')).toBeInTheDocument(); diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 7311de1cb4..fd0e73825d 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -463,6 +463,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-collection-field': `${name}.${field.name}`, 'x-component-props': { component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'], + utc: false, }, }; if (isAssocField(field)) { @@ -571,6 +572,7 @@ const associationFieldToMenu = ( interface: field.interface, }, 'x-component': 'CollectionField', + 'x-component-props': { utc: false }, 'x-read-pretty': false, 'x-decorator': 'FormItem', 'x-collection-field': `${collectionName}.${schemaName}`, @@ -686,7 +688,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': {}, + 'x-component-props': { utc: false }, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx index 1e5b4bcc2c..275c98f41b 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -89,6 +89,7 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field 'x-decorator': 'FormItem', 'x-component': 'Checkbox', 'x-content': '{{t("Show time")}}', + 'x-hidden': collectionField?.type === 'dateOnly', 'x-reactions': [ `{{(field) => { field.query('.timeFormat').take(f => { @@ -142,9 +143,10 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field const schema = { ['x-uid']: fieldSchema['x-uid'], }; - schema['x-component-props'] = fieldSchema['x-component-props'] || {}; + console.log(field.componentProps); + schema['x-component-props'] = field.componentProps || {}; fieldSchema['x-component-props'] = { - ...(fieldSchema['x-component-props'] || {}), + ...(field.componentProps || {}), ...data, }; schema['x-component-props'] = fieldSchema['x-component-props']; diff --git a/packages/core/database/src/__tests__/fields/date-only.test.ts b/packages/core/database/src/__tests__/fields/date-only.test.ts new file mode 100644 index 0000000000..1b6d9a60f7 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/date-only.test.ts @@ -0,0 +1,42 @@ +/** + * 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 { Database, mockDatabase } from '@nocobase/database'; + +describe('date only', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set date field with dateOnly', async () => { + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'dateOnly' }], + }); + + await db.sync(); + + const item = await db.getRepository('tests').create({ + values: { + date1: '2023-03-24', + }, + }); + + expect(item.get('date1')).toBe('2023-03-24'); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/date.test.ts b/packages/core/database/src/__tests__/fields/date.test.ts deleted file mode 100644 index e1a2ff189e..0000000000 --- a/packages/core/database/src/__tests__/fields/date.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * 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 { mockDatabase } from '../'; -import { Database } from '../../database'; -import { Repository } from '../../repository'; - -describe('date-field', () => { - let db: Database; - let repository: Repository; - - beforeEach(async () => { - db = mockDatabase(); - await db.clean({ drop: true }); - db.collection({ - name: 'tests', - fields: [{ name: 'date1', type: 'date' }], - }); - await db.sync(); - repository = db.getRepository('tests'); - }); - - afterEach(async () => { - await db.close(); - }); - - const createExpectToBe = async (key, actual, expected) => { - const instance = await repository.create({ - values: { - [key]: actual, - }, - }); - return expect(instance.get(key).toISOString()).toEqual(expected); - }; - - test('create', async () => { - // sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE - await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z'); - await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z'); - }); - - // dateXX 相关 Operator 都是去 time 比较的 - describe('dateOn', () => { - test('dateOn operator', async () => { - console.log('timezone', db.options.timezone); - // 默认的情况,时区为 db.options.timezone - await repository.find({ - filter: { - date1: { - // 由 db.options.timezone 来处理日期转换,假设是 +08:00 的时区 - // 2023-03-24表示的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 - $dateOn: '2023-03-24', - }, - }, - }); - - await repository.find({ - filter: { - date1: { - // +06:00 时区 2023-03-24 的范围:2023-03-23T18:00:00 ~ 2023-03-24T18:00:00 - $dateOn: '2023-03-24+06:00', - }, - }, - }); - - await repository.find({ - filter: { - date1: { - // 2023-03-23T20:00:00+08:00 在 +08:00 时区的时间是:2023-03-24 04:00:00 - // 也就是 +08:00 时区 2023-03-24 这一天的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 - $dateOn: '2023-03-23T20:00:00+08:00', - }, - }, - }); - }); - }); -}); diff --git a/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts b/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts new file mode 100644 index 0000000000..e78ada5678 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/datetime-no-tz.test.ts @@ -0,0 +1,145 @@ +/** + * 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 { Database, mockDatabase } from '@nocobase/database'; +import { sleep } from '@nocobase/test'; + +describe('datetime no tz field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+01:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should not get timezone part', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + await db.getRepository('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + const item = await db.getRepository('tests').findOne(); + expect(item.toJSON()['date1']).toBe('2023-03-24 12:00:00'); + }); + + it('should save datetime with timezone to no tz field', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + await db.getRepository('tests').create({ + values: { + date1: '2023-03-24T12:00:00.892Z', + }, + }); + + const item = await db.getRepository('tests').findOne(); + expect(item.get('date1')).toBe('2023-03-24 13:00:00'); + }); + + it('should set datetime no tz field', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeNoTz' }], + }); + + await db.sync(); + + const item = await db.getRepository('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + expect(item.get('date1')).toBe('2023-03-24 12:00:00'); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeTruthy(); + }); + + it('should set to current time when update', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + onUpdateToCurrentTime: true, + }, + { + name: 'title', + type: 'string', + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({ + values: { + title: 'test', + }, + }); + + const date1Val = instance.get('date1'); + expect(date1Val).toBeTruthy(); + + await sleep(1000); + + await c1.repository.update({ + values: { + title: 'test2', + }, + filter: { + id: instance.get('id'), + }, + }); + + await instance.reload(); + + const date1Val2 = instance.get('date1'); + expect(date1Val2).toBeTruthy(); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/datetime-tz.test.ts b/packages/core/database/src/__tests__/fields/datetime-tz.test.ts new file mode 100644 index 0000000000..7e6b7ea57b --- /dev/null +++ b/packages/core/database/src/__tests__/fields/datetime-tz.test.ts @@ -0,0 +1,250 @@ +/** + * 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 { mockDatabase } from '../'; +import { Database } from '../../database'; +import { Repository } from '../../repository'; + +describe('timezone', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should save with timezone value', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeTz' }], + }); + + await db.sync(); + + const repository = db.getRepository('tests'); + + const instance = await repository.create({ values: { date1: '2023-03-23T12:00:00.000Z' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T12:00:00.000Z'); + }); + + it('should create field with default value', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'datetimeTz', defaultValue: '2023-03-23T18:00:00.000Z' }], + }); + + let err; + try { + await db.sync(); + } catch (e) { + err = e; + } + + expect(err).toBeUndefined(); + + const repository = db.getRepository('tests'); + + const instance = await repository.create({}); + const date1 = instance.get('date1'); + + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + + describe('timezone', () => { + test('custom', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'date', timezone: '+06:00' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + + test('client', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'date', timezone: 'client' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ + values: { date1: '2023-03-24 01:00:00' }, + context: { + timezone: '+01:00', + }, + }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z'); + }); + + test('server', async () => { + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'date', timezone: 'server' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 08:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-24T00:00:00.000Z'); + }); + }); +}); + +describe('date-field', () => { + let db: Database; + let repository: Repository; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + db.collection({ + name: 'tests', + fields: [{ name: 'date1', type: 'date' }], + }); + await db.sync(); + repository = db.getRepository('tests'); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'date', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeTruthy(); + }); + + it('should set to current time when update', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'date', + onUpdateToCurrentTime: true, + }, + { + name: 'title', + type: 'string', + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({ + values: { + title: 'test', + }, + }); + + const date1Val = instance.get('date1'); + expect(date1Val).toBeDefined(); + + console.log('update'); + await c1.repository.update({ + values: { + title: 'test2', + }, + filter: { + id: instance.get('id'), + }, + }); + + await instance.reload(); + + const date1Val2 = instance.get('date1'); + expect(date1Val2).toBeDefined(); + + expect(date1Val2.getTime()).toBeGreaterThan(date1Val.getTime()); + }); + + test('create', async () => { + const createExpectToBe = async (key, actual, expected) => { + const instance = await repository.create({ + values: { + [key]: actual, + }, + }); + return expect(instance.get(key).toISOString()).toEqual(expected); + }; + + // sqlite 时区不能自定义,只有 +00:00,postgres 和 mysql 可以自定义 DB_TIMEZONE + await createExpectToBe('date1', '2023-03-24', '2023-03-24T00:00:00.000Z'); + await createExpectToBe('date1', '2023-03-24T16:00:00.000Z', '2023-03-24T16:00:00.000Z'); + }); + + // dateXX 相关 Operator 都是去 time 比较的 + describe('dateOn', () => { + test('dateOn operator', async () => { + console.log('timezone', db.options.timezone); + // 默认的情况,时区为 db.options.timezone + await repository.find({ + filter: { + date1: { + // 由 db.options.timezone 来处理日期转换,假设是 +08:00 的时区 + // 2023-03-24表示的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 + $dateOn: '2023-03-24', + }, + }, + }); + + await repository.find({ + filter: { + date1: { + // +06:00 时区 2023-03-24 的范围:2023-03-23T18:00:00 ~ 2023-03-24T18:00:00 + $dateOn: '2023-03-24+06:00', + }, + }, + }); + + await repository.find({ + filter: { + date1: { + // 2023-03-23T20:00:00+08:00 在 +08:00 时区的时间是:2023-03-24 04:00:00 + // 也就是 +08:00 时区 2023-03-24 这一天的范围:2023-03-23T16:00:00 ~ 2023-03-24T16:00:00 + $dateOn: '2023-03-23T20:00:00+08:00', + }, + }, + }); + }); + }); +}); diff --git a/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts b/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts new file mode 100644 index 0000000000..d590653811 --- /dev/null +++ b/packages/core/database/src/__tests__/fields/unix-timestamp-field.tests.ts @@ -0,0 +1,86 @@ +/** + * 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 { Database, mockDatabase } from '@nocobase/database'; +import moment from 'moment'; + +describe('unix timestamp field', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set default to current time', async () => { + const c1 = db.collection({ + name: 'test11', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + defaultToCurrentTime: true, + }, + ], + }); + + await db.sync(); + + const instance = await c1.repository.create({}); + const date1 = instance.get('date1'); + expect(date1).toBeDefined(); + + console.log(instance.toJSON()); + }); + + it('should set date value', async () => { + const c1 = db.collection({ + name: 'test12', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + }, + ], + }); + + await db.sync(); + + await c1.repository.create({ + values: { + date1: '2021-01-01T00:00:00Z', + }, + }); + + const item = await c1.repository.findOne(); + const val = item.get('date1'); + const date = moment(val).utc().format('YYYY-MM-DD HH:mm:ss'); + expect(date).toBe('2021-01-01 00:00:00'); + }); + + describe('timezone', () => { + test('custom', async () => { + db.collection({ + name: 'tests', + timestamps: false, + fields: [{ name: 'date1', type: 'unixTimestamp', timezone: '+06:00' }], + }); + + await db.sync(); + const repository = db.getRepository('tests'); + const instance = await repository.create({ values: { date1: '2023-03-24 00:00:00' } }); + const date1 = instance.get('date1'); + expect(date1.toISOString()).toEqual('2023-03-23T18:00:00.000Z'); + }); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date/date-only.test.ts b/packages/core/database/src/__tests__/operator/date/date-only.test.ts new file mode 100644 index 0000000000..aa1690b148 --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/date-only.test.ts @@ -0,0 +1,164 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('dateOnly operator', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+08:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'dateOnly', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + }); + + test('$dateBefore', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2023-01-01', + }, + }); + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2022-12-31', + }, + }); + + expect(count).toBe(0); + }); + + test('dateBetween', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01', + name: 'u0', + }, + { + date1: '2023-01-01', + name: 'u1', + }, + { + date1: '2022-12-31', + name: 'u2', + }, + { + date1: '2022-12-31', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2022-12-31'], + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts b/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts new file mode 100644 index 0000000000..4d77bb51f6 --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/datetime-no-tz.test.ts @@ -0,0 +1,201 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('datetimeNoTz date operator test', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+00:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'datetimeNoTz', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31 16:00:00', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31 16:00:01', + }, + }); + + expect(count).toBe(0); + }); + + test('$dateBefore', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2023-01-01', + }, + }); + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2022-12-31', + }, + }); + + expect(count).toBe(0); + }); + + test('$dateBefore2', async () => { + await repository.create({ + values: [ + { + date1: '2024-09-08 15:33:54', + name: 'u0', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBefore': '2024-09-08 15:33:55', + }, + }); + + expect(count).toBe(1); + }); + + test('dateBetween', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31', '2023-01-01'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31 16:00:00', '2023-01-01 00:00:00'], + }, + }); + + expect(count).toBe(4); + + count = await repository.count({ + filter: { + 'date1.$dateBetween': ['2022-12-31 11:00:00', '2022-12-31 17:00:00'], + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/operator/date-operator.test.ts b/packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts similarity index 98% rename from packages/core/database/src/__tests__/operator/date-operator.test.ts rename to packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts index f2726bd438..b722f2437e 100644 --- a/packages/core/database/src/__tests__/operator/date-operator.test.ts +++ b/packages/core/database/src/__tests__/operator/date/datetime-tz.test.ts @@ -7,9 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import Database from '../../database'; -import { Repository } from '../../repository'; -import { mockDatabase } from '../index'; +import Database from '../../../database'; +import { Repository } from '../../../repository'; +import { mockDatabase } from '../../index'; describe('date operator test', () => { let db: Database; diff --git a/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts b/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts new file mode 100644 index 0000000000..16bcde85ea --- /dev/null +++ b/packages/core/database/src/__tests__/operator/date/unix-timestamp.test.ts @@ -0,0 +1,75 @@ +import Database, { mockDatabase, Repository } from '../../../index'; + +describe('unix timestamp date operator test', () => { + let db: Database; + + let repository: Repository; + + afterEach(async () => { + await db.close(); + }); + + beforeEach(async () => { + db = mockDatabase({ + timezone: '+00:00', + }); + + await db.clean({ drop: true }); + const Test = db.collection({ + name: 'tests', + fields: [ + { + name: 'date1', + type: 'unixTimestamp', + }, + { + type: 'string', + name: 'name', + }, + ], + }); + repository = Test.repository; + await db.sync(); + }); + + test('$dateOn', async () => { + await repository.create({ + values: [ + { + date1: '2023-01-01 00:00:00', + name: 'u0', + }, + { + date1: '2023-01-01 00:00:00', + name: 'u1', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u2', + }, + { + date1: '2022-12-31 16:00:00', + name: 'u3', + }, + ], + }); + + let count: number; + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2023-01-01', + }, + }); + + expect(count).toBe(2); + + count = await repository.count({ + filter: { + 'date1.$dateOn': '2022-12-31', + }, + }); + + expect(count).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/view/view-inference.test.ts b/packages/core/database/src/__tests__/view/view-inference.test.ts index 5636dd79f5..600d3be63a 100644 --- a/packages/core/database/src/__tests__/view/view-inference.test.ts +++ b/packages/core/database/src/__tests__/view/view-inference.test.ts @@ -127,7 +127,11 @@ describe('view inference', function () { }); const createdAt = UserCollection.model.rawAttributes['createdAt'].field; - expect(inferredFields[createdAt]['type']).toBe('date'); + if (db.isMySQLCompatibleDialect()) { + expect(inferredFields[createdAt]['type']).toBe('datetimeNoTz'); + } else { + expect(inferredFields[createdAt]['type']).toBe('datetimeTz'); + } if (db.options.dialect == 'sqlite') { expect(inferredFields['name']).toMatchObject({ diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index a3b813b96b..993f1be825 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -34,7 +34,6 @@ import { import { SequelizeStorage, Umzug } from 'umzug'; import { Collection, CollectionOptions, RepositoryType } from './collection'; import { CollectionFactory } from './collection-factory'; -import { CollectionGroupManager } from './collection-group-manager'; import { ImporterReader, ImportFileExtension } from './collection-importer'; import DatabaseUtils from './database-utils'; import ReferencesMap from './features/references-map'; @@ -42,7 +41,6 @@ import { referentialIntegrityCheck } from './features/referential-integrity-chec import { ArrayFieldRepository } from './field-repository/array-field-repository'; import * as FieldTypes from './fields'; import { Field, FieldContext, RelationField } from './fields'; -import { checkDatabaseVersion } from './helpers'; import { InheritedCollection } from './inherited-collection'; import InheritanceMap from './inherited-map'; import { InterfaceManager } from './interface-manager'; @@ -221,6 +219,9 @@ export class Database extends EventEmitter implements AsyncEmitter { } } + // @ts-ignore + opts.rawTimezone = opts.timezone; + if (options.dialect === 'sqlite') { delete opts.timezone; } else if (!opts.timezone) { @@ -851,7 +852,8 @@ export class Database extends EventEmitter implements AsyncEmitter { * @internal */ async checkVersion() { - return await checkDatabaseVersion(this); + return true; + // return await checkDatabaseVersion(this); } /** diff --git a/packages/core/database/src/fields/date-field.ts b/packages/core/database/src/fields/date-field.ts index f40b27de3e..0fd68e2f2c 100644 --- a/packages/core/database/src/fields/date-field.ts +++ b/packages/core/database/src/fields/date-field.ts @@ -9,9 +9,16 @@ import { DataTypes } from 'sequelize'; import { BaseColumnFieldOptions, Field } from './field'; +import moment from 'moment'; + +const datetimeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/; + +function isValidDatetime(str) { + return datetimeRegex.test(str); +} export class DateField extends Field { - get dataType() { + get dataType(): any { return DataTypes.DATE(3); } @@ -33,6 +40,90 @@ export class DateField extends Field { return props.gmt; } + init() { + const { name, defaultToCurrentTime, onUpdateToCurrentTime, timezone } = this.options; + + this.resolveTimeZone = (context) => { + // @ts-ignore + const serverTimeZone = this.database.options.rawTimezone; + if (timezone === 'server') { + return serverTimeZone; + } + + if (timezone === 'client') { + return context?.timezone || serverTimeZone; + } + + if (timezone) { + return timezone; + } + + return serverTimeZone; + }; + + this.beforeSave = async (instance, options) => { + const value = instance.get(name); + + if (!value && instance.isNewRecord && defaultToCurrentTime) { + instance.set(name, new Date()); + return; + } + + if (onUpdateToCurrentTime) { + instance.set(name, new Date()); + return; + } + }; + + if (this.options.defaultValue && this.database.isMySQLCompatibleDialect()) { + if (typeof this.options.defaultValue === 'string' && isIso8601(this.options.defaultValue)) { + this.options.defaultValue = moment(this.options.defaultValue) + .utcOffset(this.resolveTimeZone()) + .format('YYYY-MM-DD HH:mm:ss'); + } + } + } + + setter(value, options) { + if (value === null) { + return value; + } + if (value instanceof Date) { + return value; + } + + if (typeof value === 'string' && isValidDatetime(value)) { + const dateTimezone = this.resolveTimeZone(options?.context); + const dateString = `${value} ${dateTimezone}`; + return new Date(dateString); + } + + return value; + } + + additionalSequelizeOptions() { + const { name } = this.options; + // @ts-ignore + const serverTimeZone = this.database.options.rawTimezone; + + return { + get() { + const value = this.getDataValue(name); + + if (value === null || value === undefined) { + return value; + } + + if (typeof value === 'string' && isValidDatetime(value)) { + const dateString = `${value} ${serverTimeZone}`; + return new Date(dateString); + } + + return new Date(value); + }, + }; + } + bind() { super.bind(); @@ -51,9 +142,21 @@ export class DateField extends Field { // @ts-ignore model.refreshAttributes(); } + + this.on('beforeSave', this.beforeSave); + } + + unbind() { + super.unbind(); + this.off('beforeSave', this.beforeSave); } } export interface DateFieldOptions extends BaseColumnFieldOptions { type: 'date'; } + +function isIso8601(str) { + const iso8601StrictRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + return iso8601StrictRegex.test(str); +} diff --git a/packages/core/database/src/fields/date-only-field.ts b/packages/core/database/src/fields/date-only-field.ts new file mode 100644 index 0000000000..5fce9d1b74 --- /dev/null +++ b/packages/core/database/src/fields/date-only-field.ts @@ -0,0 +1,21 @@ +/** + * 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 { BaseColumnFieldOptions, Field } from './field'; +import { DataTypes } from 'sequelize'; + +export class DateOnlyField extends Field { + get dataType(): any { + return DataTypes.DATEONLY; + } +} + +export interface DateOnlyFieldOptions extends BaseColumnFieldOptions { + type: 'dateOnly'; +} diff --git a/packages/core/database/src/fields/datetime-field.ts b/packages/core/database/src/fields/datetime-field.ts new file mode 100644 index 0000000000..12eeaff4e6 --- /dev/null +++ b/packages/core/database/src/fields/datetime-field.ts @@ -0,0 +1,17 @@ +/** + * 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 { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class DatetimeField extends DateField {} + +export interface DatetimeFieldOptions extends BaseColumnFieldOptions { + type: 'datetime'; +} diff --git a/packages/core/database/src/fields/datetime-no-tz-field.ts b/packages/core/database/src/fields/datetime-no-tz-field.ts new file mode 100644 index 0000000000..695ace4813 --- /dev/null +++ b/packages/core/database/src/fields/datetime-no-tz-field.ts @@ -0,0 +1,112 @@ +/** + * 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 { BaseColumnFieldOptions, Field } from './field'; +import { DataTypes } from 'sequelize'; +import moment from 'moment'; + +class DatetimeNoTzTypeMySQL extends DataTypes.ABSTRACT { + key = 'DATETIME'; +} + +class DatetimeNoTzTypePostgres extends DataTypes.ABSTRACT { + key = 'TIMESTAMP'; +} + +export class DatetimeNoTzField extends Field { + get dataType() { + if (this.database.inDialect('postgres')) { + return DatetimeNoTzTypePostgres; + } + + if (this.database.isMySQLCompatibleDialect()) { + return DatetimeNoTzTypeMySQL; + } + + return DataTypes.STRING; + } + + init() { + const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options; + + this.beforeSave = async (instance, options) => { + const value = instance.get(name); + + if (!value && instance.isNewRecord && defaultToCurrentTime) { + instance.set(name, new Date()); + return; + } + + if (onUpdateToCurrentTime) { + instance.set(name, new Date()); + return; + } + }; + } + + additionalSequelizeOptions(): {} { + const { name } = this.options; + + // @ts-ignore + const timezone = this.database.options.rawTimezone || '+00:00'; + + const isPg = this.database.inDialect('postgres'); + + return { + get() { + const val = this.getDataValue(name); + + if (val instanceof Date) { + if (isPg) { + return moment(val).format('YYYY-MM-DD HH:mm:ss'); + } + // format to YYYY-MM-DD HH:mm:ss + const momentVal = moment(val).utcOffset(timezone); + return momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + return val; + }, + + set(val) { + if (typeof val === 'string' && isIso8601(val)) { + const momentVal = moment(val).utcOffset(timezone); + val = momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + if (val && val instanceof Date) { + // format to YYYY-MM-DD HH:mm:ss + const momentVal = moment(val).utcOffset(timezone); + val = momentVal.format('YYYY-MM-DD HH:mm:ss'); + } + + return this.setDataValue(name, val); + }, + }; + } + + bind() { + super.bind(); + this.on('beforeSave', this.beforeSave); + } + + unbind() { + super.unbind(); + this.off('beforeSave', this.beforeSave); + } +} + +export interface DatetimeNoTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeNoTz'; +} + +function isIso8601(str) { + const iso8601StrictRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + return iso8601StrictRegex.test(str); +} diff --git a/packages/core/database/src/fields/datetime-tz-field.ts b/packages/core/database/src/fields/datetime-tz-field.ts new file mode 100644 index 0000000000..626ce1032f --- /dev/null +++ b/packages/core/database/src/fields/datetime-tz-field.ts @@ -0,0 +1,17 @@ +/** + * 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 { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class DatetimeTzField extends DateField {} + +export interface DatetimeTzFieldOptions extends BaseColumnFieldOptions { + type: 'datetimeTz'; +} diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index b9f230dd4f..e4b0b70024 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -56,7 +56,7 @@ export abstract class Field { return this.options.type; } - abstract get dataType(); + abstract get dataType(): any; isRelationField() { return false; @@ -171,11 +171,13 @@ export abstract class Field { Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) }); } + Object.assign(opts, this.additionalSequelizeOptions()); + return opts; } - isSqlite() { - return this.database.sequelize.getDialect() === 'sqlite'; + additionalSequelizeOptions() { + return {}; } typeToString() { diff --git a/packages/core/database/src/fields/index.ts b/packages/core/database/src/fields/index.ts index 610b6f1ad1..8e21d98575 100644 --- a/packages/core/database/src/fields/index.ts +++ b/packages/core/database/src/fields/index.ts @@ -36,6 +36,10 @@ import { UUIDFieldOptions } from './uuid-field'; import { VirtualFieldOptions } from './virtual-field'; import { NanoidFieldOptions } from './nanoid-field'; import { EncryptionField } from './encryption-field'; +import { UnixTimestampFieldOptions } from './unix-timestamp-field'; +import { DateOnlyFieldOptions } from './date-only-field'; +import { DatetimeNoTzField, DatetimeNoTzFieldOptions } from './datetime-no-tz-field'; +import { DatetimeTzFieldOptions } from './datetime-tz-field'; export * from './array-field'; export * from './belongs-to-field'; @@ -43,6 +47,10 @@ export * from './belongs-to-many-field'; export * from './boolean-field'; export * from './context-field'; export * from './date-field'; +export * from './datetime-field'; +export * from './datetime-tz-field'; +export * from './datetime-no-tz-field'; +export * from './date-only-field'; export * from './field'; export * from './has-many-field'; export * from './has-one-field'; @@ -61,6 +69,7 @@ export * from './uuid-field'; export * from './virtual-field'; export * from './nanoid-field'; export * from './encryption-field'; +export * from './unix-timestamp-field'; export type FieldOptions = | BaseFieldOptions @@ -81,6 +90,10 @@ export type FieldOptions = | SetFieldOptions | TimeFieldOptions | DateFieldOptions + | DatetimeTzFieldOptions + | DatetimeNoTzFieldOptions + | DateOnlyFieldOptions + | UnixTimestampFieldOptions | UidFieldOptions | UUIDFieldOptions | NanoidFieldOptions diff --git a/packages/core/database/src/fields/unix-timestamp-field.ts b/packages/core/database/src/fields/unix-timestamp-field.ts new file mode 100644 index 0000000000..f0169f2018 --- /dev/null +++ b/packages/core/database/src/fields/unix-timestamp-field.ts @@ -0,0 +1,84 @@ +/** + * 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 { DataTypes } from 'sequelize'; +import { DateField } from './date-field'; +import { BaseColumnFieldOptions } from './field'; + +export class UnixTimestampField extends DateField { + get dataType() { + return DataTypes.BIGINT; + } + + dateToValue(val) { + if (val === null || val === undefined) { + return val; + } + + let { accuracy } = this.options; + + if (this.options?.uiSchema?.['x-component-props']?.accuracy) { + accuracy = this.options?.uiSchema['x-component-props']?.accuracy; + } + + if (!accuracy) { + accuracy = 'second'; + } + + let rationalNumber = 1000; + + if (accuracy === 'millisecond') { + rationalNumber = 1; + } + + return Math.floor(new Date(val).getTime() / rationalNumber); + } + + additionalSequelizeOptions() { + const { name } = this.options; + let { accuracy } = this.options; + + if (this.options?.uiSchema?.['x-component-props']?.accuracy) { + accuracy = this.options?.uiSchema['x-component-props']?.accuracy; + } + + if (!accuracy) { + accuracy = 'second'; + } + + let rationalNumber = 1000; + + if (accuracy === 'millisecond') { + rationalNumber = 1; + } + + return { + get() { + const value = this.getDataValue(name); + if (value === null || value === undefined) { + return value; + } + + return new Date(value * rationalNumber); + }, + set(value) { + if (value === null || value === undefined) { + this.setDataValue(name, value); + } else { + // date to unix timestamp + this.setDataValue(name, Math.floor(new Date(value).getTime() / rationalNumber)); + } + }, + }; + } +} + +export interface UnixTimestampFieldOptions extends BaseColumnFieldOptions { + type: 'unixTimestamp'; +} diff --git a/packages/core/database/src/model.ts b/packages/core/database/src/model.ts index 9152ae6b39..9ec745fcca 100644 --- a/packages/core/database/src/model.ts +++ b/packages/core/database/src/model.ts @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import lodash, { isPlainObject } from 'lodash'; +import lodash from 'lodash'; import { Model as SequelizeModel, ModelStatic } from 'sequelize'; import { Collection } from './collection'; import { Database } from './database'; @@ -50,6 +50,21 @@ export class Model { - if (isDate(date)) { - return date; +const toDate = (date, options: any = {}) => { + const { ctx } = options; + const val = isDate(date) ? date : new Date(date); + const field = ctx.db.getFieldByPath(ctx.fieldPath); + + if (!field) { + return val; } - return new Date(date); + + if (field.constructor.name === 'UnixTimestampField') { + return field.dateToValue(val); + } + + if (field.constructor.name === 'DatetimeNoTzField') { + return moment(val).utcOffset('+00:00').format('YYYY-MM-DD HH:mm:ss'); + } + + if (field.constructor.name === 'DateOnlyField') { + return moment(val).format('YYYY-MM-DD HH:mm:ss'); + } + + return val; }; +function parseDateTimezone(ctx) { + const field = ctx.db.getFieldByPath(ctx.fieldPath); + + if (!field) { + return ctx.db.options.timezone; + } + + if (field.constructor.name === 'DatetimeNoTzField') { + return '+00:00'; + } + + if (field.constructor.name === 'DateOnlyField') { + return '+00:00'; + } + + return ctx.db.options.timezone; +} + +function isDatetimeString(str) { + return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(str); +} + export default { $dateOn(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); + if (typeof r === 'string') { return { - [Op.eq]: toDate(r), + [Op.eq]: toDate(r, { ctx }), }; } + if (Array.isArray(r)) { return { - [Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }], + [Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }], }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateNotOn(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.ne]: toDate(r), + [Op.ne]: toDate(r, { ctx }), }; } if (Array.isArray(r)) { return { - [Op.or]: [{ [Op.lt]: toDate(r[0]) }, { [Op.gte]: toDate(r[1]) }], + [Op.or]: [{ [Op.lt]: toDate(r[0], { ctx }) }, { [Op.gte]: toDate(r[1], { ctx }) }], }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateBefore(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); + if (typeof r === 'string') { return { - [Op.lt]: toDate(r), + [Op.lt]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.lt]: toDate(r[0]), + [Op.lt]: toDate(r[0], { ctx }), }; } + throw new Error(`Invalid Date ${JSON.stringify(value)}`); }, $dateNotBefore(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.gte]: toDate(r), + [Op.gte]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.gte]: toDate(r[0]), + [Op.gte]: toDate(r[0], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -90,15 +136,15 @@ export default { $dateAfter(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.gt]: toDate(r), + [Op.gt]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.gte]: toDate(r[1]), + [Op.gte]: toDate(r[1], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -106,15 +152,15 @@ export default { $dateNotAfter(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (typeof r === 'string') { return { - [Op.lte]: toDate(r), + [Op.lte]: toDate(r, { ctx }), }; } else if (Array.isArray(r)) { return { - [Op.lt]: toDate(r[1]), + [Op.lt]: toDate(r[1], { ctx }), }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); @@ -122,11 +168,11 @@ export default { $dateBetween(value, ctx) { const r = parseDate(value, { - timezone: ctx.db.options.timezone, + timezone: parseDateTimezone(ctx), }); if (r) { return { - [Op.and]: [{ [Op.gte]: toDate(r[0]) }, { [Op.lt]: toDate(r[1]) }], + [Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }], }; } throw new Error(`Invalid Date ${JSON.stringify(value)}`); diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index e1eab58e52..b86301dc49 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -573,7 +573,7 @@ export class Repository(values, { ...options, @@ -645,7 +645,7 @@ export class Repository exten * @internal */ public perfHistograms = new Map(); - protected plugins = new Map(); - protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); - protected _started: Date | null = null; - private _authenticated = false; - private _maintaining = false; - private _maintainingCommandStatus: MaintainingCommandStatus; - private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; - private _actionCommand: Command; - /** * @internal */ public syncManager: SyncManager; public requestLogger: Logger; + protected plugins = new Map(); + protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); + private _authenticated = false; + private _maintaining = false; + private _maintainingCommandStatus: MaintainingCommandStatus; + private _maintainingStatusBeforeCommand: MaintainingCommandStatus | null; + private _actionCommand: Command; private sqlLogger: Logger; - protected _logger: SystemLogger; constructor(public options: ApplicationOptions) { super(); @@ -241,6 +238,8 @@ export class Application exten } } + protected _started: Date | null = null; + /** * @experimental */ @@ -248,6 +247,8 @@ export class Application exten return this._started; } + protected _logger: SystemLogger; + get logger() { return this._logger; } diff --git a/packages/core/utils/src/parse-date.ts b/packages/core/utils/src/parse-date.ts index c87ff4f30b..ee55112db8 100644 --- a/packages/core/utils/src/parse-date.ts +++ b/packages/core/utils/src/parse-date.ts @@ -174,10 +174,13 @@ export function parseDate(value: any, options = {} as { timezone?: string }) { if (!value) { return; } + if (Array.isArray(value)) { return parseDateBetween(value, options); } + let timezone = options.timezone || '+00:00'; + const input = value; if (typeof value === 'string') { const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value); @@ -232,10 +235,12 @@ function parseDateBetween(value: any, options = {} as { timezone?: string }) { } const match = /(.+)((\+|\-)\d\d\:\d\d)$/.exec(value); let timezone = options.timezone || '+00:00'; + if (match) { value = match[1]; timezone = match[2]; } + const m = /^(\(|\[)(.+)\,(.+)(\)|\])$/.exec(value); if (!m) { return; diff --git a/packages/core/utils/src/parse-filter.ts b/packages/core/utils/src/parse-filter.ts index 014f64c3c3..f298afcd1e 100644 --- a/packages/core/utils/src/parse-filter.ts +++ b/packages/core/utils/src/parse-filter.ts @@ -107,6 +107,7 @@ const dateValueWrapper = (value: any, timezone?: string) => { if (!value) { return null; } + if (Array.isArray(value)) { if (value.length === 2) { value.push('[]', timezone); @@ -182,6 +183,11 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) => } if (isDateOperator(operator)) { const field = getField?.(path); + + if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') { + return value; + } + return dateValueWrapper(value, field?.timezone || timezone); } return value; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx index bead118e6a..6fe74c671a 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx @@ -153,7 +153,7 @@ export const calendarBlockSettings = new SchemaSettings({ return { title: t('End date field'), value: fieldNames.end, - options: getCollectionFieldsOptions(name, 'date', { + options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz'], { association: ['o2o', 'obo', 'oho', 'm2o'], }), onChange: (end) => { diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx index 7b1e008c22..64e937dd61 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx @@ -70,7 +70,7 @@ export const useCreateCalendarBlock = () => { const createCalendarBlock = async ({ item }) => { const stringFieldsOptions = getCollectionFieldsOptions(item.name, 'string', { dataSource: item.dataSource }); - const dateFieldsOptions = getCollectionFieldsOptions(item.name, 'date', { + const dateFieldsOptions = getCollectionFieldsOptions(item.name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz'], { association: ['o2o', 'obo', 'oho', 'm2o'], dataSource: item.dataSource, }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts index 4a30a4bd6f..1e027548be 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general3.test.ts @@ -75,7 +75,10 @@ test.describe('configure fields', () => { await addField('Attachment'); // 添加 date & time 字段 - await addField('Datetime'); + await addField('Datetime(with time zone)'); + await addField('Datetime(without time zone)'); + await addField('DateOnly'); + await addField('Unix Timestamp'); await addField('Time'); // 添加 relation 字段 diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts index f858128762..4b81aa3dba 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/utils.ts @@ -430,8 +430,11 @@ export type FieldInterface = | 'Markdown' | 'Rich Text' | 'Attachment' - | 'Datetime' + | 'Datetime(with time zone)' + | 'Datetime(without time zone)' + | 'Date' | 'Time' + | 'Unix Timestamp' | 'One to one (belongs to)' | 'One to one (has one)' | 'One to many' diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts new file mode 100644 index 0000000000..86e0f326fd --- /dev/null +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/fields/datetime.test.ts @@ -0,0 +1,51 @@ +import Database, { Collection as DBCollection } from '@nocobase/database'; +import Application from '@nocobase/server'; +import { createApp } from '../index'; + +describe('datetime', () => { + let db: Database; + let app: Application; + let Collection: DBCollection; + let Field: DBCollection; + + beforeEach(async () => { + app = await createApp(); + db = app.db; + Collection = db.getCollection('collections'); + Field = db.getCollection('fields'); + }); + + afterEach(async () => { + await app.destroy(); + }); + + it('should create datetimeNoTz field', async () => { + await Collection.repository.create({ + values: { + name: 'tests', + fields: [ + { + type: 'datetimeNoTz', + name: 'date1', + }, + ], + }, + context: {}, + }); + + // @ts-ignore + const agent = app.agent(); + + const createRes = await agent.resource('tests').create({ + values: { + date1: '2023-03-24 12:00:00', + }, + }); + + expect(createRes.status).toBe(200); + + // get item + const res = await agent.resource('tests').list(); + expect(res.body.data[0].date1).toBe('2023-03-24 12:00:00'); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx index 57a25ac964..5d3e26f986 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx @@ -38,7 +38,7 @@ export const SetFilterTargetKey = (props) => { const interfaceOptions = app.dataSourceManager.collectionFieldInterfaceManager.getFieldInterface( field.interface, ); - if (interfaceOptions.titleUsable) { + if (interfaceOptions?.titleUsable) { return true; } return false; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx index 403015f0f6..7ea3db0021 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/components/FieldType.tsx @@ -11,12 +11,13 @@ import { observer } from '@formily/react'; import { useRecord } from '@nocobase/client'; import { Select, Tag } from 'antd'; import React from 'react'; +import { omit } from 'lodash'; export const FieldType = observer( (props: any) => { const { value, handleFieldChange, onChange } = props; const record = useRecord(); - const item = record; + const item = omit(record, ['__parent', '__collectionName']); return !item?.possibleTypes ? ( {value} ) : ( diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx index eb5a12ea28..4c2dcb3cfd 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/CollectionFields.tsx @@ -81,13 +81,13 @@ const tableContainer = css` } td, th { - flex: 2; + flex: 2.3; width: 0; &:nth-child(5) { flex: 1.2; } &:last-child { - flex: 1.8; + flex: 1.5; } } .ant-table-selection-column, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx index 2916d9128f..336420ccc3 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/Gantt.Settings.tsx @@ -366,7 +366,7 @@ export const ganttSettings = new SchemaSettings({ return { title: t('Start date field'), value: fieldNames.start, - options: useOptions('date'), + options: useOptions(['date', 'datetime', 'dateOnly', 'datetimeNoTz']), onChange: (start) => { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['start'] = start; @@ -398,7 +398,7 @@ export const ganttSettings = new SchemaSettings({ return { title: t('End date field'), value: fieldNames.end, - options: useOptions('date'), + options: useOptions(['date', 'datetime', 'dateOnly', 'datetimeNoTz']), onChange: (end) => { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['end'] = end; diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx index 310b65638b..d09a0bdb74 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/GanttBlockInitializer.tsx @@ -80,7 +80,7 @@ export const useCreateGanttBlock = () => { }; }); const dateFields = collectionFields - ?.filter((field) => field.type === 'date') + ?.filter((field) => ['date', 'datetime', 'dateOnly', 'datetimeNoTz'].includes(field.type)) ?.map((field) => { return { label: field?.uiSchema?.title, diff --git a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx index be011ba1c3..38a6e30007 100644 --- a/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx +++ b/packages/plugins/@nocobase/plugin-gantt/src/client/utils.tsx @@ -13,11 +13,17 @@ import { useTranslation } from 'react-i18next'; export const useGanttTranslation = () => { return useTranslation('gantt'); }; -export const useOptions = (type = 'string') => { +export const useOptions = (type: string | string[] = 'string') => { const compile = useCompile(); const { fields } = useCollection_deprecated(); const options = fields - ?.filter((field) => field.type === type) + ?.filter((field) => { + if (typeof type === 'string') { + return field.type === type; + } else { + return type.includes(field.type); + } + }) ?.map((field) => { return { value: field.name, diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts index b8f6586d37..2b8871b5a6 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/customFormBlocks1.test.ts @@ -565,7 +565,7 @@ test.describe('field data entry', () => { await page .locator(`button[aria-label^="schema-initializer-Grid-workflowManual:customForm:configureFields-${randomValue}"]`) .hover(); - await page.getByRole('menuitem', { name: 'Datetime', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Datetime(with time zone)' }).click(); await page .getByLabel(`block-item-Input-${randomValue}-Field display name`) .getByRole('textbox') From 5112b8e77e8568d9870e0bf9b3dca83f388a46ca Mon Sep 17 00:00:00 2001 From: Katherine Date: Tue, 10 Sep 2024 16:48:59 +0800 Subject: [PATCH 20/68] refactor: secondary confirmation information supports customization (#5199) * refactor: secondary confirmation information supports customization * fix: bug * fix: bug * refactor: locale improve * refactor: locale improve * fix: test * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * refactor: locale improve * fix: test * fix: bug --- lerna.json | 4 +- packages/core/client/src/locale/en_US.json | 4 +- packages/core/client/src/locale/zh-CN.json | 14 +++- .../__e2e__/bulk-destroy/basic.test.ts | 2 + .../antd/action/Action.Designer.tsx | 84 ++++++++++++++----- .../schema-component/antd/action/Action.tsx | 9 +- 6 files changed, 88 insertions(+), 29 deletions(-) diff --git a/lerna.json b/lerna.json index 0ecbef7cdf..9710afb03d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.4.0-alpha", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json index 6ba6e86e55..bed21dd5b4 100644 --- a/packages/core/client/src/locale/en_US.json +++ b/packages/core/client/src/locale/en_US.json @@ -840,5 +840,7 @@ "is none of": "is none of", "is any of": "is any of", "Plugin dependency version mismatch": "Plugin dependency version mismatch", - "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?" + "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?", + "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data": "Skip getting the total number of table records during paging to speed up loading. It is recommended to enable this option for data tables with a large amount of data", + "Enable secondary confirmation": "Enable secondary confirmation" } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 6d09e9f9f3..87aeb5c96b 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -981,5 +981,17 @@ "Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间", "Datetime(with time zone)": "日期时间(含时区)", "Datetime(without time zone)": "日期时间(不含时区)", - "DateOnly":"仅日期" + "DateOnly":"仅日期", + "Enable secondary confirmation": "启用二次确认", + "Content": "内容", + "Perform the Update record": "执行更新数据", + "Are you sure you want to perform the Update record action?": "你确定执行更新数据操作吗?", + "Perform the Custom request": "执行自定义请求", + "Are you sure you want to perform the Custom request action":"你确定执行自定义请求操作吗?", + "Perform the Refresh":"执行刷新", + "Are you sure you want to perform the Refresh action?":"你确定执行刷新操作吗?", + "Perform the Submit":"执行提交", + "Are you sure you want to perform the Submit action?":"你确定执行提交操作吗?", + "Perform the Trigger workflow":"执行触发工作流", + "Are you sure you want to perform the Trigger workflow action?":"你确定执行触发工作流吗?" } diff --git a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts index 25886da626..777b9bb5c1 100644 --- a/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/bulk-destroy/basic.test.ts @@ -44,6 +44,8 @@ test.describe('bulk-destroy', () => { await page.getByLabel('action-Action-Delete-destroy-').hover(); await page.getByLabel('designer-schema-settings-Action-actionSettings:bulkDelete-general').hover(); await page.getByRole('menuitem', { name: 'Secondary confirmation' }).click(); + await page.getByLabel('Enable secondary confirmation').uncheck(); + await page.getByRole('button', { name: 'OK' }).click(); await page.mouse.move(500, 0); // 2. 选中所有行 diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index fa88fab0e5..441cd653e6 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -819,34 +819,80 @@ export function SecondConFirm() { const { dn } = useDesignable(); const fieldSchema = useFieldSchema(); const { t } = useTranslation(); - const field = useField(); + const field = useField(); + const compile = useCompile(); return ( - { - if (!fieldSchema['x-component-props']) { - fieldSchema['x-component-props'] = {}; - } - if (value) { - fieldSchema['x-component-props'].confirm = value - ? { - title: 'Perform the {{title}}', - content: 'Are you sure you want to perform the {{title}} action?', - } - : {}; - } else { - fieldSchema['x-component-props'].confirm = {}; - } - field.componentProps.confirm = { ...fieldSchema['x-component-props']?.confirm }; + initialValues={{ + title: + compile(fieldSchema?.['x-component-props']?.confirm?.title) || + t('Perform the {{title}}', { title: compile(fieldSchema.title) }), + content: + compile(fieldSchema?.['x-component-props']?.confirm?.content) || + t('Are you sure you want to perform the {{title}} action?', { title: compile(fieldSchema.title) }), + }} + schema={ + { + type: 'object', + title: t('Secondary confirmation'), + properties: { + enable: { + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + 'x-content': t('Enable secondary confirmation'), + default: + fieldSchema?.['x-component-props']?.confirm?.enable !== false && + !!fieldSchema?.['x-component-props']?.confirm?.content, + 'x-component-props': {}, + }, + title: { + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + title: t('Title'), + 'x-reactions': { + dependencies: ['enable'], + fulfill: { + state: { + required: '{{$deps[0]}}', + }, + }, + }, + }, + content: { + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + title: t('Content'), + 'x-reactions': { + dependencies: ['enable'], + fulfill: { + state: { + required: '{{$deps[0]}}', + }, + }, + }, + }, + }, + } as ISchema + } + onSubmit={({ enable, title, content }) => { + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props'].confirm = {}; + fieldSchema['x-component-props'].confirm.enable = enable; + fieldSchema['x-component-props'].confirm.title = title; + fieldSchema['x-component-props'].confirm.content = content; + field.componentProps.confirm = { ...fieldSchema['x-component-props']?.confirm }; dn.emit('patch', { schema: { ['x-uid']: fieldSchema['x-uid'], - 'x-component-props': { ...fieldSchema['x-component-props'] }, + 'x-component-props': { + ...fieldSchema['x-component-props'], + }, }, }); + dn.refresh(); }} /> ); 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 898b08cb19..3174959b97 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -55,7 +55,6 @@ export const Action: ComposedAction = withDynamicSchemaProps( observer((props: ActionProps) => { const { popover, - confirm, containerRefKey, component, useAction = useA, @@ -68,6 +67,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( openSize: os, disabled: propsDisabled, actionCallback, + confirm: propsConfirm, /** 如果为 true 则说明该按钮是树表格的 Add child 按钮 */ addChild, onMouseEnter, @@ -94,7 +94,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( const openMode = fieldSchema?.['x-component-props']?.['openMode']; const openSize = fieldSchema?.['x-component-props']?.['openSize']; const refreshDataBlockRequest = fieldSchema?.['x-component-props']?.['refreshDataBlockRequest']; - + const confirm = compile(fieldSchema['x-component-props']?.confirm) || propsConfirm; const disabled = form.disabled || field.disabled || field.data?.disabled || propsDisabled || disableAction; const linkageRules = useMemo(() => fieldSchema?.['x-linkage-rules'] || [], [fieldSchema?.['x-linkage-rules']]); const { designable } = useDesignable(); @@ -178,7 +178,6 @@ export const Action: ComposedAction = withDynamicSchemaProps( }; const buttonElement = RenderButton(buttonProps); - // if (!btnHover) { // return buttonElement; // } @@ -313,7 +312,6 @@ function RenderButton({ const { t } = useTranslation(); const { isPopupVisibleControlledByURL } = usePopupSettings(); const { openPopup } = usePopupUtils(); - const handleButtonClick = useCallback( (e: React.MouseEvent, checkPortal = true) => { if (checkPortal && isPortalInBody(e.target as Element)) { @@ -347,7 +345,7 @@ function RenderButton({ } } }; - if (confirm?.content) { + if (confirm?.enable !== false && confirm?.content) { modal.confirm({ title: t(confirm.title, { title: confirmTitle || actionTitle }), content: t(confirm.content, { title: confirmTitle || actionTitle }), @@ -363,6 +361,7 @@ function RenderButton({ actionTitle, confirm?.content, confirm?.title, + confirm?.enable, disabled, modal, onClick, From 0d32ba4ff5000062a80dbd0bc061e9aeb71c3230 Mon Sep 17 00:00:00 2001 From: Katherine Date: Tue, 10 Sep 2024 21:51:37 +0800 Subject: [PATCH 21/68] chore: support composite record unique key (#5007) * refactor: filterTargetkey support mutiple * refactor: getRowKey * fix: bug * chore: test * chore: test * chore: test * fix: bug * fix: build * fix: useBulkDestroyActionProps support join primary key * fix: build * fix: bug * fix: bug * fix: bug * fix: bug * fix: fieldNames * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * refactor: sourceIdValue * fix: remoteselect * chore: test * chore: multi target key in has many relation repository * chore: test * chore: multiple relation repository * fix: test * refactor: target collection not support join collection * Update update-associations.ts --------- Co-authored-by: Chareice Co-authored-by: chenos --- packages/core/actions/src/actions/move.ts | 8 +- .../src/block-provider/BlockProvider.tsx | 11 +- .../src/block-provider/TableFieldProvider.tsx | 3 +- .../block-provider/TableSelectorProvider.tsx | 3 +- .../client/src/block-provider/hooks/index.ts | 16 +- .../Configuration/EditCollectionAction.tsx | 3 + .../src/collection-manager/interfaces/m2m.tsx | 2 +- .../src/collection-manager/interfaces/m2o.tsx | 2 +- .../src/collection-manager/interfaces/o2m.tsx | 7 +- .../src/collection-manager/interfaces/o2o.tsx | 2 +- .../interfaces/utils/index.ts | 20 ++ .../src/collection-manager/templates/sql.tsx | 3 + .../src/collection-manager/templates/view.tsx | 3 + .../collection/CollectionManager.ts | 23 ++- .../data-block/DataBlockResourceProvider.tsx | 13 +- .../table/hooks/useTableBlockProps.tsx | 4 +- .../antd/association-field/InternalPicker.tsx | 2 +- .../schema-component/antd/page/PagePopups.tsx | 24 ++- .../antd/remote-select/RemoteSelect.tsx | 4 +- .../schema-component/antd/table-v2/Table.tsx | 20 +- .../multi-filter-target-key.test.ts | 187 ++++++++++++++++++ .../multi-filter-target-key.test.ts | 145 ++++++++++++++ packages/core/database/src/collection.ts | 11 +- packages/core/database/src/database.ts | 8 +- packages/core/database/src/options-parser.ts | 39 +++- .../relation-repository/hasmany-repository.ts | 21 +- .../multiple-relation-repository.ts | 19 +- .../relation-repository.ts | 48 ++++- packages/core/database/src/repository.ts | 53 +++-- .../core/database/src/update-associations.ts | 4 +- .../server/sql-collection/sql-collection.ts | 29 +-- .../EditCollectionAction.tsx | 3 + .../CollectionsManager/SetFilterTargetKey.tsx | 9 +- .../Configuration/ConfigurationTable.tsx | 10 +- .../plugin-field-m2m-array/src/client/mbm.ts | 2 +- 35 files changed, 653 insertions(+), 108 deletions(-) create mode 100644 packages/core/client/src/collection-manager/interfaces/utils/index.ts create mode 100644 packages/core/database/src/__tests__/filter-target-key/multi-filter-target-key.test.ts create mode 100644 packages/core/database/src/__tests__/relation-repository/multi-filter-target-key.test.ts diff --git a/packages/core/actions/src/actions/move.ts b/packages/core/actions/src/actions/move.ts index 9081b298c1..a1d57a5a1c 100644 --- a/packages/core/actions/src/actions/move.ts +++ b/packages/core/actions/src/actions/move.ts @@ -75,8 +75,8 @@ export class SortAbleCollection { // insert source position to target position async move(sourceInstanceId: TargetKey, targetInstanceId: TargetKey, options: MoveOptions = {}) { - const sourceInstance = await this.collection.repository.findById(sourceInstanceId); - const targetInstance = await this.collection.repository.findById(targetInstanceId); + const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId); + const targetInstance = await this.collection.repository.findByTargetKey(targetInstanceId); if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) { await sourceInstance.update({ @@ -88,7 +88,7 @@ export class SortAbleCollection { } async changeScope(sourceInstanceId: TargetKey, targetScope: any, method?: string) { - const sourceInstance = await this.collection.repository.findById(sourceInstanceId); + const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId); const targetScopeValue = targetScope[this.scopeKey]; if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) { @@ -108,7 +108,7 @@ export class SortAbleCollection { } async sticky(sourceInstanceId: TargetKey) { - const sourceInstance = await this.collection.repository.findById(sourceInstanceId); + const sourceInstance = await this.collection.repository.findByTargetKey(sourceInstanceId); await sourceInstance.update( { [this.field.get('name')]: 0, diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 8c817f0093..66f289a1a0 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -11,6 +11,7 @@ import { Field, GeneralField } from '@formily/core'; import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { Col, Row } from 'antd'; import merge from 'deepmerge'; +import { isArray } from 'lodash'; import template from 'lodash/template'; import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; @@ -307,7 +308,15 @@ export const useFilterByTk = () => { const association = getCollectionField(assoc); return recordData?.[association.targetKey || 'id']; } - return recordData?.[collection.filterTargetKey || 'id']; + if (isArray(collection.filterTargetKey)) { + const filterByTk = {}; + for (const key of collection.filterTargetKey) { + filterByTk[key] = recordData?.[key]; + } + return filterByTk; + } else { + return recordData?.[collection.filterTargetKey || 'id']; + } }; /** diff --git a/packages/core/client/src/block-provider/TableFieldProvider.tsx b/packages/core/client/src/block-provider/TableFieldProvider.tsx index 2fab38ae73..6fa1dcce1d 100644 --- a/packages/core/client/src/block-provider/TableFieldProvider.tsx +++ b/packages/core/client/src/block-provider/TableFieldProvider.tsx @@ -183,9 +183,10 @@ export const useTableFieldProps = () => { rowKey: (record: any) => { return field.value?.indexOf?.(record); }, - onRowSelectionChange(selectedRowKeys) { + onRowSelectionChange(selectedRowKeys, selectedRowData) { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; }, onChange({ current, pageSize }) { ctx.service.run({ page: current, pageSize }); diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 3ddcc01d4a..9c8e9b72e1 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -319,9 +319,10 @@ export const useTableSelectorProps = () => { dragSort: false, rowKey: ctx.rowKey || 'id', pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination, - onRowSelectionChange(selectedRowKeys, selectedRows) { + onRowSelectionChange(selectedRowKeys, selectedRowData) { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; }, async onRowDragEnd({ from, to }) { await ctx.resource.move({ diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 25f1d9457a..2bb08bea2c 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1080,13 +1080,25 @@ export const useBulkDestroyActionProps = () => { const { field } = useBlockRequestContext(); const { resource, service } = useBlockRequestContext(); const { setSubmitted } = useActionContext(); + const collection = useCollection_deprecated(); + const { filterTargetKey } = collection; return { async onClick(e?, callBack?) { + let filterByTk = field.data?.selectedRowKeys; + if (Array.isArray(filterTargetKey)) { + filterByTk = field.data.selectedRowData.map((v) => { + const obj = {}; + filterTargetKey.map((j) => { + obj[j] = v[j]; + }); + return obj; + }); + } if (!field?.data?.selectedRowKeys?.length) { return; } await resource.destroy({ - filterByTk: field.data?.selectedRowKeys, + filterByTk, }); field.data.selectedRowKeys = []; const currentPage = service.params[0]?.page; @@ -1098,7 +1110,7 @@ export const useBulkDestroyActionProps = () => { callBack?.(); } setSubmitted?.(true); - // service?.refresh?.(); + service?.refresh?.(); }, }; }; diff --git a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx index 6bf545b4d8..dc314f823e 100644 --- a/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/EditCollectionAction.tsx @@ -64,6 +64,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, footer: { diff --git a/packages/core/client/src/collection-manager/interfaces/m2m.tsx b/packages/core/client/src/collection-manager/interfaces/m2m.tsx index cd00ad9045..bbec128358 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2m.tsx @@ -10,7 +10,7 @@ import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { defaultProps, relationshipType, reverseFieldProperties } from './properties'; export class M2MFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/m2o.tsx b/packages/core/client/src/collection-manager/interfaces/m2o.tsx index b6ad7c8267..8cf58654c7 100644 --- a/packages/core/client/src/collection-manager/interfaces/m2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/m2o.tsx @@ -9,7 +9,7 @@ import { ISchema } from '@formily/react'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; export class M2OFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/o2m.tsx b/packages/core/client/src/collection-manager/interfaces/o2m.tsx index 2b55318b5b..bfd664a03d 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2m.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2m.tsx @@ -8,10 +8,9 @@ */ import { ISchema } from '@formily/react'; -import { Collection } from '../../data-source'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; - +import { getUniqueKeyFromCollection } from './utils'; export class O2MFieldInterface extends CollectionFieldInterface { name = 'o2m'; type = 'object'; @@ -215,7 +214,3 @@ export class O2MFieldInterface extends CollectionFieldInterface { ], }; } - -export function getUniqueKeyFromCollection(collection: Collection) { - return collection?.filterTargetKey || collection?.getPrimaryKey() || 'id'; -} diff --git a/packages/core/client/src/collection-manager/interfaces/o2o.tsx b/packages/core/client/src/collection-manager/interfaces/o2o.tsx index f47de65b75..16f497ed17 100644 --- a/packages/core/client/src/collection-manager/interfaces/o2o.tsx +++ b/packages/core/client/src/collection-manager/interfaces/o2o.tsx @@ -9,7 +9,7 @@ import { ISchema } from '@formily/react'; import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { getUniqueKeyFromCollection } from './o2m'; +import { getUniqueKeyFromCollection } from './utils'; import { constraintsProps, relationshipType, reverseFieldProperties } from './properties'; export class O2OFieldInterface extends CollectionFieldInterface { diff --git a/packages/core/client/src/collection-manager/interfaces/utils/index.ts b/packages/core/client/src/collection-manager/interfaces/utils/index.ts new file mode 100644 index 0000000000..3984230cc4 --- /dev/null +++ b/packages/core/client/src/collection-manager/interfaces/utils/index.ts @@ -0,0 +1,20 @@ +/** + * 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 { Collection } from '../../../data-source'; + +export function getUniqueKeyFromCollection(collection: Collection) { + if (collection?.filterTargetKey) { + if (Array.isArray(collection.filterTargetKey)) { + return collection?.filterTargetKey?.[0]; + } + return collection?.filterTargetKey; + } + return collection?.getPrimaryKey() || 'id'; +} diff --git a/packages/core/client/src/collection-manager/templates/sql.tsx b/packages/core/client/src/collection-manager/templates/sql.tsx index 0ca28e4727..405a20547e 100644 --- a/packages/core/client/src/collection-manager/templates/sql.tsx +++ b/packages/core/client/src/collection-manager/templates/sql.tsx @@ -90,6 +90,9 @@ export class SqlCollectionTemplate extends CollectionTemplate { description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, }; diff --git a/packages/core/client/src/collection-manager/templates/view.tsx b/packages/core/client/src/collection-manager/templates/view.tsx index 82f2d6ac9b..197fbda053 100644 --- a/packages/core/client/src/collection-manager/templates/view.tsx +++ b/packages/core/client/src/collection-manager/templates/view.tsx @@ -153,6 +153,9 @@ export class ViewCollectionTemplate extends CollectionTemplate { description: `{{t( "If a collection lacks a primary key, you must configure a unique record key to locate row records within a block, failure to configure this will prevent the creation of data blocks for the collection.")}}`, 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, 'x-reactions': ['{{useAsyncDataSource(loadFilterTargetKeys)}}'], }, ...getConfigurableProperties('category', 'description'), diff --git a/packages/core/client/src/data-source/collection/CollectionManager.ts b/packages/core/client/src/data-source/collection/CollectionManager.ts index 0d9265973f..6f8bda7966 100644 --- a/packages/core/client/src/data-source/collection/CollectionManager.ts +++ b/packages/core/client/src/data-source/collection/CollectionManager.ts @@ -8,6 +8,7 @@ */ import type { SchemaKey } from '@formily/json-schema'; +import qs from 'qs'; import type { DataSource } from '../data-source'; import type { CollectionFieldOptions, CollectionOptions, GetCollectionFieldPredicate } from './Collection'; @@ -159,10 +160,23 @@ export class CollectionManager { ); return; } + const getTargetKey = (collection: Collection) => collection.filterTargetKey || collection.getPrimaryKey() || 'id'; + + const buildFilterByTk = (targetKey: string | string[], record: Record) => { + if (Array.isArray(targetKey)) { + const filterByTk = {}; + targetKey.forEach((key) => { + filterByTk[key] = record[key]; + }); + return qs.stringify(filterByTk); + } else { + return record[targetKey]; + } + }; if (collectionOrAssociation instanceof Collection) { - const key = collectionOrAssociation.filterTargetKey || collectionOrAssociation.getPrimaryKey() || 'id'; - return collectionRecordOrAssociationRecord[key]; + const targetKey = getTargetKey(collectionOrAssociation); + return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); } if (collectionOrAssociation.includes('.')) { @@ -186,9 +200,8 @@ export class CollectionManager { ); return; } - - const key = targetCollection?.filterTargetKey || targetCollection?.getPrimaryKey() || 'id'; - return collectionRecordOrAssociationRecord[key]; + const targetKey = getTargetKey(targetCollection); + return buildFilterByTk(targetKey, collectionRecordOrAssociationRecord); } getSourceKeyByAssociation(associationName: string) { diff --git a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx index 6625dc9277..9bb4d8df08 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockResourceProvider.tsx @@ -9,7 +9,7 @@ import { IResource } from '@nocobase/sdk'; import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react'; - +import { isArray } from 'lodash'; import { useAPIClient } from '../../api-client'; import { useCollectionManager } from '../collection'; import { CollectionRecord } from '../collection-record'; @@ -34,7 +34,16 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr if (association && parentRecord) { const sourceKey = cm.getSourceKeyByAssociation(association); const parentRecordData = parentRecord instanceof CollectionRecord ? parentRecord.data : parentRecord; - return parentRecordData[sourceKey]; + if (isArray(sourceKey)) { + const filterByTk = {}; + for (const key of sourceKey) { + filterByTk[key] = parentRecordData?.[key]; + } + + return encodeURIComponent(JSON.stringify(filterByTk)); + } else { + return parentRecordData[sourceKey]; + } } }, [association, sourceId, parentRecord]); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx index bd5dbad023..2fd25fe32f 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/table/hooks/useTableBlockProps.tsx @@ -16,7 +16,6 @@ import { findFilterTargets } from '../../../../../block-provider/hooks'; import { DataBlock, useFilterBlock } from '../../../../../filter-provider/FilterProvider'; import { mergeFilter } from '../../../../../filter-provider/utils'; import { removeNullCondition } from '../../../../../schema-component'; -import { useCollection } from '../../../../../data-source'; export const useTableBlockProps = () => { const field = useField(); @@ -58,9 +57,10 @@ export const useTableBlockProps = () => { dragSort: ctx.dragSort && ctx.dragSortBy, rowKey: ctx.rowKey || 'id', pagination: fieldSchema?.['x-component-props']?.pagination === false ? false : field.componentProps.pagination, - onRowSelectionChange: useCallback((selectedRowKeys) => { + onRowSelectionChange: useCallback((selectedRowKeys, selectedRowData) => { ctx.field.data = ctx?.field?.data || {}; ctx.field.data.selectedRowKeys = selectedRowKeys; + ctx.field.data.selectedRowData = selectedRowData; ctx?.field?.onRowSelect?.(selectedRowKeys); }, []), onRowDragEnd: useCallback( diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx index 81acd1b72b..452ad80585 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalPicker.tsx @@ -53,7 +53,7 @@ export const useTableSelectorProps = () => { type: multiple ? 'checkbox' : 'radio', selectedRowKeys: rcSelectRows ?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])) - .map((item) => item[rowKey]), + .map((item) => item[rowKey]?.toString()), }, onRowSelectionChange(selectedRowKeys, selectedRows) { if (multiple) { diff --git a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx index dc0aa0f44c..db65ea1070 100644 --- a/packages/core/client/src/schema-component/antd/page/PagePopups.tsx +++ b/packages/core/client/src/schema-component/antd/page/PagePopups.tsx @@ -169,7 +169,7 @@ const PagePopupsItemProvider: FC<{ collection={params.collection || context.collection} association={context.association} sourceId={params.sourceid} - filterByTk={params.filterbytk} + filterByTk={parseQueryString(params.filterbytk)} // @ts-ignore record={storedContext.record} parentRecord={storedContext.parentRecord} @@ -465,3 +465,25 @@ function findSchemaByUid(uid: string, rootSchema: Schema, resultRef: { value: Sc }); return resultRef.value; } + +function parseQueryString(queryString) { + // 如果没有 '&',直接返回原始字符串 + if (!queryString?.includes?.('=')) { + return queryString; + } + + // 解码查询字符串 + const decodedString = decodeURIComponent(queryString); + + // 将解码后的字符串按 '&' 分隔成键值对 + const pairs = decodedString.split('&'); + + // 将键值对转换为对象 + const params = pairs.reduce((acc, pair) => { + const [key, value] = pair.split('='); + acc[key] = value; + return acc; + }, {}); + + return params; +} diff --git a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx index baacd15c7c..4f6ca2e6b2 100644 --- a/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx +++ b/packages/core/client/src/schema-component/antd/remote-select/RemoteSelect.tsx @@ -92,7 +92,9 @@ const InternalRemoteSelect = connect( (options) => { try { return options - .filter((v) => ['number', 'string'].includes(typeof v[fieldNames.value])) + .filter((v) => { + return ['number', 'string'].includes(typeof v[fieldNames.value]) || !v[fieldNames.value]; + }) .map((option) => { let label = compile(option[fieldNames.label]); diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index a06ba65b6d..505c38d89f 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -521,6 +521,9 @@ export const Table: any = withDynamicSchemaProps( * @returns */ const defaultRowKey = useCallback((record: any) => { + if (rowKey) { + return getRowKey(record); + } if (record.key) { return record.key; } @@ -536,13 +539,21 @@ export const Table: any = withDynamicSchemaProps( const getRowKey = useCallback( (record: any) => { - if (typeof rowKey === 'string') { + if (Array.isArray(rowKey)) { + // 使用多个字段值组合生成唯一键 + return rowKey + .map((keyField) => { + return record[keyField]?.toString() || ''; + }) + .join('-'); + } else if (typeof rowKey === 'string') { return record[rowKey]?.toString(); } else { + // 如果 rowKey 是函数或未提供,使用 defaultRowKey return (rowKey ?? defaultRowKey)(record)?.toString(); } }, - [rowKey, defaultRowKey], + [JSON.stringify(rowKey), defaultRowKey], ); const dataSourceKeys = field?.value?.map?.(getRowKey); @@ -623,6 +634,7 @@ export const Table: any = withDynamicSchemaProps( onChange(selectedRowKeys: any[], selectedRows: any[]) { field.data = field.data || {}; field.data.selectedRowKeys = selectedRowKeys; + field.data.selectedRowData = selectedRows; setSelectedRowKeys(selectedRowKeys); onRowSelectionChange?.(selectedRowKeys, selectedRows); }, @@ -720,7 +732,7 @@ export const Table: any = withDynamicSchemaProps( const rowClassName = useCallback( (record) => (selectedRow.includes(record[rowKey]) ? highlightRow : ''), - [selectedRow, highlightRow, rowKey], + [selectedRow, highlightRow, JSON.stringify(rowKey)], ); const onExpandValue = useCallback( @@ -771,7 +783,7 @@ export const Table: any = withDynamicSchemaProps( { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('should set multi filter target keys', async () => { + const Student = db.collection({ + name: 'students', + autoGenId: false, + filterTargetKey: ['name', 'classId'], + fields: [ + { + name: 'name', + type: 'string', + primaryKey: true, + }, + { + name: 'classId', + type: 'bigInt', + primaryKey: true, + }, + { + name: 'age', + type: 'integer', + }, + ], + }); + + await db.sync(); + + const s1 = await Student.repository.create({ + values: { + name: 's1', + classId: 1, + age: 10, + }, + }); + + // it should find by name and classId + const findRes = await Student.repository.find({ + filterByTk: { + name: 's1', + classId: 1, + }, + }); + + expect(findRes.length).toBe(1); + + // update + await Student.repository.update({ + filterByTk: { + name: 's1', + classId: 1, + }, + values: { + age: '20', + }, + }); + + const s1Updated = await Student.repository.findOne({ + filterByTk: { + name: 's1', + classId: 1, + }, + }); + + expect(s1Updated.age).toBe(20); + + // destroy + await Student.repository.destroy({ + filterByTk: { + name: 's1', + classId: 1, + }, + }); + + expect(await Student.repository.count()).toBe(0); + }); + + it('should save multi filter target keys association', async () => { + const Class = db.collection({ + name: 'classes', + autoGenId: true, + fields: [ + { + name: 'name', + type: 'string', + }, + { + name: 'students', + type: 'hasMany', + target: 'students', + foreignKey: 'classId', + targetKey: 'name', + }, + ], + }); + + const Student = db.collection({ + name: 'students', + autoGenId: false, + filterTargetKey: ['key1', 'key2'], + fields: [ + { + name: 'name', + type: 'string', + primaryKey: true, + }, + { + name: 'key1', + type: 'bigInt', + }, + { + name: 'key2', + type: 'string', + }, + { + name: 'age', + type: 'integer', + }, + ], + }); + + await db.sync(); + + const s1 = await Student.repository.create({ + values: { + name: 's1', + key1: 1, + key2: 'k1', + }, + }); + + const s2 = await Student.repository.create({ + values: { + name: 's2', + key1: 2, + key2: 'k2', + }, + }); + + const c1 = await Class.repository.create({ + values: { + name: 'c1', + students: [ + { + name: 's1', + key1: 1, + key2: 'k1', + }, + { + name: 's2', + key1: 2, + key2: 'k2', + }, + ], + }, + }); + + const c1Students = await Class.repository.find({ + filterByTk: { + name: 'c1', + }, + appends: ['students'], + }); + + expect(c1Students[0].students.length).toBe(2); + }); +}); diff --git a/packages/core/database/src/__tests__/relation-repository/multi-filter-target-key.test.ts b/packages/core/database/src/__tests__/relation-repository/multi-filter-target-key.test.ts new file mode 100644 index 0000000000..0b6790c452 --- /dev/null +++ b/packages/core/database/src/__tests__/relation-repository/multi-filter-target-key.test.ts @@ -0,0 +1,145 @@ +/** + * 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 Database, { HasManyRepository, mockDatabase } from '../../index'; +describe('multi target key in association repository', () => { + let db: Database; + + beforeEach(async () => { + db = mockDatabase(); + await db.clean({ drop: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + describe('use multi target key as source key', async () => { + test('has many repository', async () => { + const Student = db.collection({ + name: 'students', + autoGenId: false, + filterTargetKey: ['name', 'classId'], + fields: [ + { + name: 'name', + type: 'string', + primaryKey: true, + }, + { + name: 'classId', + type: 'bigInt', + primaryKey: true, + }, + { + name: 'age', + type: 'integer', + }, + { + name: 'id', + type: 'uid', + }, + { + name: 'books', + type: 'hasMany', + target: 'books', + foreignKey: 'studentId', + targetKey: 'id', + }, + ], + }); + + const Book = db.collection({ + name: 'books', + autoGenId: false, + filterTargetKey: ['name', 'author'], + fields: [ + { + name: 'name', + type: 'string', + primaryKey: true, + }, + { + name: 'author', + type: 'string', + primaryKey: true, + }, + { + name: 'student', + type: 'belongsTo', + foreignKey: 'studentId', + targetKey: 'id', + }, + ], + }); + + await db.sync(); + + await Student.repository.create({ + values: [ + { + name: 's1', + classId: 1, + age: 10, + books: [ + { + name: 'b1', + author: 'a1', + }, + { + name: 'b2', + author: 'a1', + }, + ], + }, + { + name: 's2', + classId: 2, + age: 11, + books: [ + { + name: 'b3', + author: 'a1', + }, + ], + }, + ], + }); + + const hasManyRepo = db.getRepository( + 'students.books', + encodeURIComponent(JSON.stringify({ name: 's1', classId: 1 })), + ); + + const res = await hasManyRepo.find(); + + expect(res.length).toBe(2); + + const b2a1 = await hasManyRepo.findOne({ + filterByTk: { + name: 'b2', + author: 'a1', + }, + }); + + expect(b2a1).toBeDefined(); + expect(b2a1.get('name')).toBe('b2'); + expect(b2a1.get('author')).toBe('a1'); + + await db + .getRepository('students.books', { + name: 's2', + classId: 2, + }) + .destroy({}); + + expect(await db.getRepository('books').count()).toBe(2); + }); + }); +}); diff --git a/packages/core/database/src/collection.ts b/packages/core/database/src/collection.ts index afd9571323..059189ed96 100644 --- a/packages/core/database/src/collection.ts +++ b/packages/core/database/src/collection.ts @@ -98,7 +98,7 @@ export interface CollectionOptions extends Omit viewName?: string; writableView?: boolean; - filterTargetKey?: string; + filterTargetKey?: string | string[]; fields?: FieldOptions[]; model?: string | ModelStatic; repository?: string | RepositoryType; @@ -169,14 +169,21 @@ export class Collection< this.setSortable(options.sortable); } - get filterTargetKey() { + get filterTargetKey(): string | string[] { const targetKey = this.options?.filterTargetKey; + + if (Array.isArray(targetKey)) { + return targetKey; + } + if (targetKey && this.model.getAttributes()[targetKey]) { return targetKey; } + if (this.model.primaryKeyAttributes.length > 1) { return null; } + return this.model.primaryKeyAttribute; } diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 993f1be825..70c1c463cf 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -53,7 +53,7 @@ import extendOperators from './operators'; import QueryInterface from './query-interface/query-interface'; import buildQueryInterface from './query-interface/query-interface-builder'; import { RelationRepository } from './relation-repository/relation-repository'; -import { Repository } from './repository'; +import { Repository, TargetKey } from './repository'; import { AfterDefineCollectionListener, BeforeDefineCollectionListener, @@ -634,11 +634,11 @@ export class Database extends EventEmitter implements AsyncEmitter { getRepository(name: string): R; - getRepository(name: string, relationId: string | number): R; + getRepository(name: string, relationId: TargetKey): R; - getRepository(name: string, relationId: string | number): R; + getRepository(name: string, relationId: TargetKey): R; - getRepository(name: string, relationId?: string | number): Repository | R { + getRepository(name: string, relationId?: TargetKey): Repository | R { const [collection, relation] = name.split('.'); if (relation) { return this.getRepository(collection)?.relation(relation)?.of(relationId) as R; diff --git a/packages/core/database/src/options-parser.ts b/packages/core/database/src/options-parser.ts index cd2eb87a8e..5ac11a3b17 100644 --- a/packages/core/database/src/options-parser.ts +++ b/packages/core/database/src/options-parser.ts @@ -76,17 +76,42 @@ export class OptionsParser { return this.isAssociation(path.split('.')[0]); } + filterByTkToWhereOption() { + const filterByTkOption = this.options?.filterByTk; + + if (!filterByTkOption) { + return {}; + } + + // multi filter target key + if (lodash.isPlainObject(this.options.filterByTk)) { + const where = {}; + for (const [key, value] of Object.entries(filterByTkOption)) { + where[key] = value; + } + + return where; + } + + // single filter target key + const filterTargetKey = this.context.targetKey || this.collection.filterTargetKey; + + if (Array.isArray(filterTargetKey)) { + throw new Error('multi filter target key value must be object'); + } + + return { + [filterTargetKey]: filterByTkOption, + }; + } + toSequelizeParams() { const queryParams = this.filterParser.toSequelizeParams(); if (this.options?.filterByTk) { + const filterByTkWhere = this.filterByTkToWhereOption(); queryParams.where = { - [Op.and]: [ - queryParams.where, - { - [this.context.targetKey || this.collection.filterTargetKey]: this.options.filterByTk, - }, - ], + [Op.and]: [queryParams.where, filterByTkWhere], }; } @@ -115,7 +140,7 @@ export class OptionsParser { let defaultSortField = this.model.primaryKeyAttribute; - if (!defaultSortField && this.collection.filterTargetKey) { + if (!defaultSortField && this.collection.filterTargetKey && !Array.isArray(this.collection.filterTargetKey)) { defaultSortField = this.collection.filterTargetKey; } diff --git a/packages/core/database/src/relation-repository/hasmany-repository.ts b/packages/core/database/src/relation-repository/hasmany-repository.ts index 376eff9435..8a5974f2ee 100644 --- a/packages/core/database/src/relation-repository/hasmany-repository.ts +++ b/packages/core/database/src/relation-repository/hasmany-repository.ts @@ -17,18 +17,20 @@ export class HasManyRepository extends MultipleRelationRepository { async find(options?: FindOptions): Promise { const targetRepository = this.targetCollection.repository; - const addFilter = { - [this.association.foreignKey]: this.sourceKeyValue, - }; + const targetFilterOptions = await this.targetRepositoryFilterOptionsBySourceValue(); - if (options?.filterByTk) { - addFilter[this.associationField.targetKey] = options.filterByTk; + const findOptionsOmit = ['where', 'values', 'attributes']; + + if (options?.filterByTk && !this.isMultiTargetKey(options.filterByTk)) { + // @ts-ignore + targetFilterOptions[this.associationField.targetKey] = options.filterByTk; + findOptionsOmit.push('filterByTk'); } const findOptions = { - ...omit(options, ['filterByTk', 'where', 'values', 'attributes']), + ...omit(options, findOptionsOmit), filter: { - $and: [options.filter || {}, addFilter], + $and: [options?.filter || {}, targetFilterOptions], }, }; @@ -37,14 +39,11 @@ export class HasManyRepository extends MultipleRelationRepository { async aggregate(options: AggregateOptions) { const targetRepository = this.targetCollection.repository; - const addFilter = { - [this.association.foreignKey]: this.sourceKeyValue, - }; const aggOptions = { ...options, filter: { - $and: [options.filter || {}, addFilter], + $and: [options.filter || {}, await this.targetRepositoryFilterOptionsBySourceValue()], }, }; diff --git a/packages/core/database/src/relation-repository/multiple-relation-repository.ts b/packages/core/database/src/relation-repository/multiple-relation-repository.ts index 30da978d8f..66226ac3a0 100644 --- a/packages/core/database/src/relation-repository/multiple-relation-repository.ts +++ b/packages/core/database/src/relation-repository/multiple-relation-repository.ts @@ -32,6 +32,21 @@ export interface AssociatedOptions extends Transactionable { } export abstract class MultipleRelationRepository extends RelationRepository { + async targetRepositoryFilterOptionsBySourceValue(): Promise { + let filterForeignKeyValue = this.sourceKeyValue; + + if (this.isMultiTargetKey()) { + const sourceModel = await this.getSourceModel(); + + // @ts-ignore + filterForeignKeyValue = sourceModel.get(this.association.sourceKey); + } + + return { + [this.association.foreignKey]: filterForeignKeyValue, + }; + } + async find(options?: FindOptions): Promise { const targetRepository = this.targetCollection.repository; @@ -49,9 +64,7 @@ export abstract class MultipleRelationRepository extends RelationRepository { const appendFilter = { isPivotFilter: true, association: pivotAssoc, - where: { - [association.foreignKey]: this.sourceKeyValue, - }, + where: await this.targetRepositoryFilterOptionsBySourceValue(), }; return targetRepository.find({ diff --git a/packages/core/database/src/relation-repository/relation-repository.ts b/packages/core/database/src/relation-repository/relation-repository.ts index cf7dee06f5..f7ddd1363d 100644 --- a/packages/core/database/src/relation-repository/relation-repository.ts +++ b/packages/core/database/src/relation-repository/relation-repository.ts @@ -16,7 +16,7 @@ import { RelationField } from '../fields/relation-field'; import FilterParser from '../filter-parser'; import { Model } from '../model'; import { OptionsParser } from '../options-parser'; -import { CreateOptions, Filter, FindOptions } from '../repository'; +import { CreateOptions, Filter, FindOptions, TargetKey } from '../repository'; import { updateAssociations } from '../update-associations'; import { UpdateGuard } from '../update-guard'; @@ -31,17 +31,19 @@ export abstract class RelationRepository { targetCollection: Collection; associationName: string; associationField: RelationField; - sourceKeyValue: string | number; + sourceKeyValue: TargetKey; sourceInstance: Model; db: Database; database: Database; - constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number) { + constructor(sourceCollection: Collection, association: string, sourceKeyValue: TargetKey) { this.db = sourceCollection.context.database; this.database = this.db; this.sourceCollection = sourceCollection; - this.sourceKeyValue = sourceKeyValue; + + this.setSourceKeyValue(sourceKeyValue); + this.associationName = association; this.association = this.sourceCollection.model.associations[association]; @@ -51,6 +53,24 @@ export abstract class RelationRepository { this.targetCollection = this.sourceCollection.context.database.modelCollection.get(this.targetModel); } + decodeMultiTargetKey(str: string) { + try { + const decoded = decodeURIComponent(str); + return JSON.parse(decoded); + } catch (e) { + return false; + } + } + + setSourceKeyValue(sourceKeyValue: TargetKey) { + this.sourceKeyValue = + typeof sourceKeyValue === 'string' ? this.decodeMultiTargetKey(sourceKeyValue) || sourceKeyValue : sourceKeyValue; + } + + isMultiTargetKey(value?: any) { + return lodash.isPlainObject(value || this.sourceKeyValue); + } + get collection() { return this.db.getCollection(this.targetModel.name); } @@ -145,12 +165,20 @@ export abstract class RelationRepository { async getSourceModel(transaction?: Transaction) { if (!this.sourceInstance) { - this.sourceInstance = await this.sourceCollection.model.findOne({ - where: { - [this.associationField.sourceKey]: this.sourceKeyValue, - }, - transaction, - }); + this.sourceInstance = this.isMultiTargetKey() + ? await this.sourceCollection.repository.findOne({ + filter: { + // @ts-ignore + ...this.sourceKeyValue, + }, + transaction, + }) + : await this.sourceCollection.model.findOne({ + where: { + [this.associationField.sourceKey]: this.sourceKeyValue, + }, + transaction, + }); } return this.sourceInstance; diff --git a/packages/core/database/src/repository.ts b/packages/core/database/src/repository.ts index b86301dc49..c033510077 100644 --- a/packages/core/database/src/repository.ts +++ b/packages/core/database/src/repository.ts @@ -59,7 +59,10 @@ export interface FilterAble { filter: Filter; } -export type TargetKey = string | number; +export type BaseTargetKey = string | number; +export type MultiTargetKey = Record; +export type TargetKey = BaseTargetKey | MultiTargetKey; + export type TK = TargetKey | TargetKey[]; type FieldValue = string | number | bigint | boolean | Date | Buffer | null | FieldValue[] | FilterWithOperator; @@ -211,7 +214,7 @@ class RelationRepositoryBuilder { } } - of(id: string | number): R { + of(id: TargetKey): R { if (!this.association) { return; } @@ -312,13 +315,12 @@ export class Repository any }): Promise { @@ -504,6 +504,10 @@ export class Repository 1) { + return null; + } + return this.model.primaryKeyAttribute; + } + isSql() { return true; } @@ -32,18 +49,6 @@ export class SQLCollection extends Collection { return undefined; } - /* istanbul ignore next -- @preserve */ - get filterTargetKey() { - const targetKey = this.options?.filterTargetKey || 'id'; - if (targetKey && this.model.getAttributes()[targetKey]) { - return targetKey; - } - if (this.model.primaryKeyAttributes.length > 1) { - return null; - } - return this.model.primaryKeyAttribute; - } - modelInit() { const { autoGenId, sql } = this.options; const model = class extends SQLModel {}; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx index 09ac2749b7..afe3eb3cfc 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/EditCollectionAction.tsx @@ -77,6 +77,9 @@ const getSchema = (schema: IField, record: any, compile, getContainer): ISchema ), 'x-decorator': 'FormItem', 'x-component': 'Select', + 'x-component-props': { + multiple: true, + }, enum: '{{filterTargetKeyOptions}}', }, footer: { diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx index 5d3e26f986..8cff8f2831 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/SetFilterTargetKey.tsx @@ -62,13 +62,16 @@ export const SetFilterTargetKey = (props) => { {size === 'small' ?
: ' '} { + const format = getPickerFormat(value); + field.setComponentProps({ + picker: value, + format, + }); + newProps.picker = value; + newProps.format = format; + setStateProps(newProps); + fieldSchema['x-component-props'] = { + ...props, + picker: value, + format, + }; + field.value = undefined; + }} + /> + + + ); + } return ; }; +DatePicker.FilterWithPicker = function FilterWithPicker(props: any) { + const { picker = 'date', format } = props; + const { utc = true } = useDatePickerContext(); + const value = Array.isArray(props.value) ? props.value[0] : props.value; + const compile = useCompile(); + const fieldSchema = useFieldSchema(); + const targetPicker = value ? inferPickerType(value) : picker; + const targetFormat = getPickerFormat(targetPicker) || format; + const newProps = { + utc, + ...props, + underFilter: true, + showTime: props.showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false, + format: targetFormat, + picker: targetPicker, + }; + const field: any = useField(); + const [stateProps, setStateProps] = useState(newProps); + return ( + + { if (e.target.value) { setCustomFormatPreview(date.format(e.target.value)); @@ -86,19 +100,22 @@ const InternalExpiresRadio = (props) => { if (isCustom) { onChange(e.target.value); } + setTargetValue(e.target.value); }} /> ); } - return ( - - - {v.label} - - - ); + if (!picker || picker === 'date') { + return ( + + + {v.label} + + + ); + } })} @@ -113,4 +130,4 @@ const ExpiresRadio = connect( }), ); -export { ExpiresRadio }; +export { ExpiresRadio, DateFormatCom }; diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 2c5062344e..79025dabd2 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -65,6 +65,7 @@ export const DynamicComponent = (props: Props) => { ...props.style, }, utc: false, + underFilter: true, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx index 9143ab64f4..a5700807d2 100644 --- a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -496,7 +496,6 @@ export const EditOperator = () => { const { dn } = useDesignable(); const operatorList = useOperatorList(); const { getOperator, collectOperator } = useOperators(); - if (operatorList.length && !getOperator(fieldSchema.name)) { collectOperator(fieldSchema.name, operatorList[0].value); } @@ -512,22 +511,22 @@ export const EditOperator = () => { _.set(fieldSchema, 'x-filter-operator', v); const operator = operatorList.find((item) => item.value === v); - let componentProps = {}; - + let componentProps = { ...fieldSchema['x-component-props'] }; + field.value = undefined; //切换操作符清空字段值 // 根据操作符的配置,设置组件的属性 - if (operator?.schema?.['x-component']) { + if (operator?.schema?.['x-component'] && !operator?.onlyFilterAction) { _.set(fieldSchema, 'x-component-props.component', operator.schema?.['x-component']); _.set(field, 'componentProps.component', operator.schema?.['x-component']); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: operator.schema['x-component'], ...operator.schema?.['x-component-props'], }; } else if (fieldSchema['x-component-props']?.component) { _.set(fieldSchema, 'x-component-props.component', null); _.set(field, 'componentProps.component', null); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: null, ...operator.schema?.['x-component-props'], }; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index ba15dd74b3..36b5d5bdbe 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -62,5 +62,6 @@ export * from './variable'; export * from './unix-timestamp'; export * from './nanoid-input'; export * from './error-fallback'; +export * from './expiresRadio'; import './index.less'; diff --git a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx index 05728552ac..2ff4c514f4 100644 --- a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx +++ b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx @@ -11,11 +11,12 @@ import { connect, mapProps, mapReadPretty } from '@formily/react'; import React from 'react'; import ReactQuill from 'react-quill'; import { isVariable } from '../../../variables/utils/isVariable'; -import { ReadPretty as InputReadPretty } from '../input'; +import { ReadPretty as InputReadPretty, Input } from '../input'; import { useStyles } from './style'; export const RichText = connect( (props) => { + const { underFilter } = props; const { wrapSSR, hashId, componentCls } = useStyles(); const modules = { toolbar: [['bold', 'italic', 'underline', 'link'], [{ list: 'ordered' }, { list: 'bullet' }], ['clean']], @@ -35,7 +36,9 @@ export const RichText = connect( ]; const { value, defaultValue, onChange, disabled } = props; const resultValue = isVariable(value || defaultValue) ? undefined : value || defaultValue || ''; - + if (underFilter) { + return ; + } return wrapSSR( { const { value, onChange } = props; - return ( dayjs(date)); - b = b.map((date) => dayjs(date)); - - return a[0].isBetween(b[0], b[1], null, '[]') && a[1].isBetween(b[0], b[1], null, '[]'); + return a === b; }, $dateBefore: function (a, b) { if (!a || !b) { return false; } - if (!Array.isArray(a)) { - a = [a, a]; - } - if (!Array.isArray(b)) { - b = [b, b]; - } - a = a.map((date) => dayjs(date)); - b = b.map((date) => dayjs(date)); + // Parse both date strings + const dateA = parseDate(a); + const dateB = parseDate(b); - return a[0].isBefore(b[0]) && a[1].isBefore(b[0]); + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + return dateA < dateB; }, $dateNotBefore: function (a, b) { - return !operations.$dateBefore(a, b); + if (!a || !b) { + return false; + } + const dateA = parseDate(a); + const dateB = parseDate(b); + + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + + // Compare the two dates + return dateA >= dateB; }, $dateAfter: function (a, b) { if (!a || !b) { return false; } - if (!Array.isArray(a)) { - a = [a, a]; - } - if (!Array.isArray(b)) { - b = [b, b]; - } - a = a.map((date) => dayjs(date)); - b = b.map((date) => dayjs(date)); + // Parse both date strings + const dateA = parseDate(a); + const dateB = parseDate(b); - return a[0].isAfter(b[1]) && a[1].isAfter(b[1]); + return dateA > dateB; }, $dateNotAfter: function (a, b) { - return !operations.$dateAfter(a, b); + if (!a || !b) { + return false; + } + const dateA = parseDate(a); + const dateB = parseDate(b); + + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + return dateA <= dateB; }, $dateBetween: function (a, b) { - return operations.$dateOn(a, b); + if (!a || !b) { + return false; + } + const dateA = parseFullDate(a); + const dateBStart = parseFullDate(b[0]); + const dateBEnd = parseFullDate(b[1]); + + if (!dateA || !dateBStart || !dateBEnd) { + throw new Error('Invalid date format'); + } + return dateA >= dateBStart && dateA <= dateBEnd; }, $dateNotOn: function (a, b) { - return !operations.$dateOn(a, b); + if (!a || !b) { + return false; + } + return a !== b; }, $isTruly: function (a) { if (Array.isArray(a)) return a.some((k) => k === true || k === 1); @@ -615,3 +632,43 @@ export function getJsonLogic() { return jsonLogic; } + +function parseFullDate(dateStr) { + return new Date(dateStr); +} + +function parseMonth(dateStr) { + const [year, month] = dateStr.split('-').map(Number); + return new Date(year, month - 1); +} + +function parseQuarter(dateStr) { + const year = parseInt(dateStr.slice(0, 4)); + const quarter = parseInt(dateStr.slice(5, 6)); + const month = (quarter - 1) * 3; + return new Date(year, month); +} + +function parseYear(dateStr) { + const year = parseInt(dateStr); + return new Date(year, 0); +} + +function parseDate(dateStr) { + dateStr = dateStr.trim(); + + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + // It's in "YYYY-MM-DD" format + return parseFullDate(dateStr); + } else if (/^\d{4}-\d{2}$/.test(dateStr)) { + // It's in "YYYY-MM" format + return parseMonth(dateStr); + } else if (/^\d{4}Q[1-4]$/.test(dateStr)) { + // It's in "YYYYQn" format + return parseQuarter(dateStr); + } else if (/^\d{4}$/.test(dateStr)) { + // It's in "YYYY" format + return parseYear(dateStr); + } + return null; // Invalid format +} diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index a0e174a3b4..c305551283 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -11,11 +11,13 @@ import Handlebars from 'handlebars'; import { dayjs } from '@nocobase/utils/client'; import helpers from '@budibase/handlebars-helpers'; import _, { every, findIndex, some } from 'lodash'; +import { getPickerFormat } from '@nocobase/utils/client'; import { replaceVariableValue } from '../../../block-provider/hooks'; import { VariableOption, VariablesContextType } from '../../../variables/types'; import { isVariable } from '../../../variables/utils/isVariable'; import { transformVariableValue } from '../../../variables/utils/transformVariableValue'; import { getJsonLogic } from '../../common/utils/logic'; +import { inferPickerType } from '../../antd/date-picker/util'; import url from 'url'; type VariablesCtx = { /** 当前登录的用户 */ @@ -124,11 +126,19 @@ export const conditionAnalyses = async ({ const jsonLogic = getJsonLogic(); const [value, targetValue] = await Promise.all(parsingResult); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); + let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); + const comparisonValue = transformVariableValue(value, { targetCollectionField }); + if ( + ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) && + currentInputValue + ) { + const picker = inferPickerType(comparisonValue); + const format = getPickerFormat(picker); + currentInputValue = dayjs(currentInputValue).format(format); + } + return jsonLogic.apply({ - [operator]: [ - transformVariableValue(targetValue, { targetCollectionField }), - transformVariableValue(value, { targetCollectionField }), - ], + [operator]: [currentInputValue, comparisonValue], }); } catch (error) { throw error; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index fd0e73825d..c99cb91bf6 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -462,8 +462,8 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, 'x-component-props': { - component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'], utc: false, + underFilter: true, }, }; if (isAssocField(field)) { @@ -478,7 +478,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-decorator': 'FormItem', 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': field.uiSchema?.['x-component-props'], + 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false, underFilter: true }, }; } const resultItem = { @@ -572,7 +572,7 @@ const associationFieldToMenu = ( interface: field.interface, }, 'x-component': 'CollectionField', - 'x-component-props': { utc: false }, + 'x-component-props': { utc: false, underFilter: true }, 'x-read-pretty': false, 'x-decorator': 'FormItem', 'x-collection-field': `${collectionName}.${schemaName}`, @@ -688,7 +688,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': { utc: false }, + 'x-component-props': { utc: false, underFilter: true }, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx index 275c98f41b..33d2c3379a 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -11,13 +11,14 @@ import { css } from '@emotion/css'; import { ISchema, Schema, useField } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { getPickerFormat } from '@nocobase/utils/client'; import { useCollectionManager_deprecated, useDesignable } from '..'; -import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio'; +import { DateFormatCom, ExpiresRadio } from '../schema-component'; import { SchemaSettingsModalItem } from './SchemaSettings'; export const SchemaSettingsDateFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { const { fieldSchema } = props; - const field = useField(); + const field: any = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const { getCollectionJoinField } = useCollectionManager_deprecated(); @@ -31,13 +32,48 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field fieldSchema?.['x-component-props']?.timeFormat || collectionField?.uiSchema?.['x-component-props']?.timeFormat || 'HH:mm:ss'; + const pickerDefaultValue = + fieldSchema?.['x-component-props']?.picker || collectionField?.uiSchema?.['x-component-props']?.picker || 'date'; + const isReadPretty = fieldSchema['x-read-pretty'] || field.readOnly || field.readPretty; return ( { - const schema = { + const schema: any = { ['x-uid']: fieldSchema['x-uid'], }; - console.log(field.componentProps); + if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty && field.value) { + field.value = undefined; + field.initialValue = undefined; + fieldSchema.default = undefined; + schema.default = undefined; + } schema['x-component-props'] = field.componentProps || {}; fieldSchema['x-component-props'] = { ...(field.componentProps || {}), @@ -157,6 +222,10 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field const modifiedString = parts.join('.'); field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => { if (f.props.name === fieldSchema.name) { + if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty) { + f.value = undefined; + f.initialValue = undefined; + } f.setComponentProps({ ...data }); } }); diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx index f1fe305b7e..08069f4b1c 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx @@ -110,7 +110,11 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: VariableInput: (inputProps) => { return ( - + ); }, diff --git a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts index f5f2a5bf7c..f413b0c9ba 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts +++ b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts @@ -48,7 +48,10 @@ import { filterFormBlockSettings } from '../modules/blocks/filter-blocks/form/fi import { filterFormItemFieldSettings } from '../modules/blocks/filter-blocks/form/filterFormItemFieldSettings'; import { markdownBlockSettings } from '../modules/blocks/other-blocks/markdown/markdownBlockSettings'; import { cascadeSelectComponentFieldSettings } from '../modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings'; -import { datePickerComponentFieldSettings } from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; +import { + datePickerComponentFieldSettings, + rangePickerPickerComponentFieldSettings, +} from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; import { fileManagerComponentFieldSettings } from '../modules/fields/component/FileManager/fileManagerComponentFieldSettings'; import { previewComponentFieldSettings } from '../modules/fields/component/FileManager/previewComponentFieldSettings'; import { uploadAttachmentComponentFieldSettings } from '../modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings'; @@ -120,6 +123,7 @@ export class SchemaSettingsPlugin extends Plugin { this.schemaSettingsManager.add(subformPopoverComponentFieldSettings); this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings); this.schemaSettingsManager.add(datePickerComponentFieldSettings); + this.schemaSettingsManager.add(rangePickerPickerComponentFieldSettings); this.schemaSettingsManager.add(unixTimestampComponentFieldSettings); this.schemaSettingsManager.add(inputNumberComponentFieldSettings); this.schemaSettingsManager.add(inputComponentSettings); diff --git a/packages/core/utils/src/date.ts b/packages/core/utils/src/date.ts index 3a7bad0485..731c23a1f9 100644 --- a/packages/core/utils/src/date.ts +++ b/packages/core/utils/src/date.ts @@ -44,7 +44,7 @@ export const getDefaultFormat = (props: GetDefaultFormatProps) => { } else if (props['picker'] === 'year') { return 'YYYY'; } else if (props['picker'] === 'week') { - return 'YYYY-wo'; + return 'YYYY[W]W'; } return props['showTime'] ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; }; @@ -68,24 +68,33 @@ export const toLocal = (value: dayjs.Dayjs) => { } }; +const convertQuarterToFirstDay = (quarterStr) => { + const year = parseInt(quarterStr.slice(0, 4)); // 提取年份 + const quarter = parseInt(quarterStr.slice(-1)); // 提取季度数字 + return dayjs().quarter(quarter).year(year); +}; + const toMoment = (val: any, options?: Str2momentOptions) => { if (!val) { return; } const offset = options.utcOffset || -1 * new Date().getTimezoneOffset(); const { gmt, picker, utc = true } = options; + if (dayjs(val).isValid()) { + if (!utc) { + return dayjs(val); + } - if (!utc) { - return dayjs(val); + if (dayjs.isDayjs(val)) { + return val.utcOffset(offsetFromString(offset)); + } + if (gmt) { + return dayjs(val).utcOffset(0); + } + return dayjs(val).utcOffset(offsetFromString(offset)); + } else { + return convertQuarterToFirstDay(val); } - - if (dayjs.isDayjs(val)) { - return val.utcOffset(offsetFromString(offset)); - } - if (gmt || picker) { - return dayjs(val).utcOffset(0); - } - return dayjs(val).utcOffset(offsetFromString(offset)); }; export const str2moment = ( @@ -198,3 +207,18 @@ function absFloor(number) { return Math.floor(number); } } + +export const getPickerFormat = (picker) => { + switch (picker) { + case 'week': + return 'YYYY[W]W'; + case 'month': + return 'YYYY-MM'; + case 'quarter': + return 'YYYY[Q]Q'; + case 'year': + return 'YYYY'; + default: + return 'YYYY-MM-DD'; + } +}; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx index 1d4c4cdfad..0ed93b3552 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx @@ -99,7 +99,6 @@ export const BulkEditField = (props: any) => { const [value, setValue] = useState(null); const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema.name) || {}; - useEffect(() => { field.value = toFormFieldValue({ [type]: value }); if (field.required) { @@ -112,6 +111,12 @@ export const BulkEditField = (props: any) => { } }, [field, type, value]); + useEffect(() => { + if (field.value === null) { + setValue(undefined); + } + }, [field.value]); + const typeChangeHandler = (val) => { setType(val); field.required = val === BulkEditFormItemValueType.ChangedTo; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx index 03704b08c3..1cbe5476c1 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx @@ -30,6 +30,7 @@ import { ResourceActionContext, useDataSourceManager, } from '@nocobase/client'; +import { getPickerFormat } from '@nocobase/utils/client'; import { message } from 'antd'; import { getCollectionSchema } from './schema/collections'; import { CollectionFields } from './CollectionFields'; @@ -208,6 +209,7 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', + getPickerFormat, }} /> diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx index 2032a92027..816f7c23d6 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx @@ -12,7 +12,6 @@ import { action } from '@formily/reactive'; import { uid } from '@formily/shared'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -// import { CollectionFieldsTable } from '.'; import { useAPIClient, useCurrentAppInfo, @@ -29,9 +28,8 @@ import { CollectionCategoriesContext, FieldSummary, TemplateSummary, - useRequest, - useCollectionRecordData, } from '@nocobase/client'; +import { getPickerFormat } from '@nocobase/utils/client'; import { CollectionFields } from './CollectionFields'; import { collectionSchema } from './schemas/collections'; @@ -239,6 +237,7 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', + getPickerFormat, }} /> From 6dee46440a0097b938bae2db3f0447f71ff51fcf Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Mon, 30 Sep 2024 06:21:57 +0800 Subject: [PATCH 64/68] chore(e2e): skip one test --- packages/core/client/src/modules/variable/__e2e__/basic.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 901f4fbf2b..0ae864aeac 100644 --- a/packages/core/client/src/modules/variable/__e2e__/basic.test.ts +++ b/packages/core/client/src/modules/variable/__e2e__/basic.test.ts @@ -44,7 +44,7 @@ test.describe('variables', () => { await expect(page.getByRole('textbox')).toHaveValue(token); }); - test('Table selected records', async ({ page, mockPage, mockRecord }) => { + test.skip('Table selected records', async ({ page, mockPage, mockRecord }) => { const nocoPage = await mockPage(tableSelectedRecords).waitForInit(); const record = await mockRecord('testTableSelectedRecords'); await nocoPage.goto(); From c91c43a71b5958d2405175b97b08a05060bfa318 Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 30 Sep 2024 08:41:51 +0800 Subject: [PATCH 65/68] Revert "feat: date type fields support setting the picker type (#5271)" This reverts commit dcaad79370b18ae17747e7fb1a1eb146783f88a1. --- .../collection-manager/interfaces/dateOnly.ts | 24 ++- .../interfaces/properties/index.ts | 99 +----------- .../interfaces/properties/operators.ts | 49 +----- .../interfaces/unixTimestamp.tsx | 3 +- .../CollectionFieldInterface.ts | 11 -- packages/core/client/src/locale/zh-CN.json | 5 +- .../datePickerComponentFieldSettings.tsx | 18 --- .../antd/date-picker/DatePicker.tsx | 142 +----------------- .../antd/date-picker/ReadPretty.tsx | 9 +- .../__tests__/date-picker.test.tsx | 3 + .../__tests__/mapDatePicker.test.ts | 26 ++-- .../antd/date-picker/__tests__/util.test.ts | 8 +- .../schema-component/antd/date-picker/util.ts | 81 ++-------- .../antd/filter/DynamicComponent.tsx | 1 - .../antd/form-item/SchemaSettingOptions.tsx | 11 +- .../client/src/schema-component/antd/index.ts | 1 - .../antd/rich-text/RichText.tsx | 7 +- .../antd/unix-timestamp/UnixTimestamp.tsx | 1 + .../schema-component/common/utils/logic.js | 121 ++++----------- .../schema-component/common/utils/uitls.tsx | 18 +-- .../client/src/schema-initializer/utils.ts | 8 +- .../DateFormat/ExpiresRadio.tsx} | 47 ++---- .../SchemaSettingsDateFormat.tsx | 77 +--------- .../SchemaSettingsDefaultValue.tsx | 6 +- .../schema-settings/SchemaSettingsPlugin.ts | 6 +- packages/core/utils/src/date.ts | 46 ++---- .../src/client/component/BulkEditField.tsx | 7 +- .../CollectionsManager/ConfigurationTable.tsx | 2 - .../Configuration/ConfigurationTable.tsx | 5 +- 29 files changed, 164 insertions(+), 678 deletions(-) rename packages/core/client/src/{schema-component/antd/expiresRadio/index.tsx => schema-settings/DateFormat/ExpiresRadio.tsx} (71%) diff --git a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts index 7bc57cbb60..defeb80b16 100644 --- a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts +++ b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts @@ -31,12 +31,26 @@ export class DateFieldInterface extends CollectionFieldInterface { hasDefaultValue = true; properties = { ...defaultProps, - ...dateTimeProps, - 'uiSchema.x-component-props.showTime': { - type: 'boolean', + 'uiSchema.x-component-props.dateFormat': { + type: 'string', + title: '{{t("Date format")}}', + 'x-component': 'Radio.Group', 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - 'x-visible': false, + default: 'YYYY-MM-DD', + enum: [ + { + label: '{{t("Year/Month/Day")}}', + value: 'YYYY/MM/DD', + }, + { + label: '{{t("Year-Month-Day")}}', + value: 'YYYY-MM-DD', + }, + { + label: '{{t("Day/Month/Year")}}', + value: 'DD/MM/YYYY', + }, + ], }, }; filterable = { diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index 82d08c2ad9..edb01ef255 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -10,8 +10,6 @@ import { Field } from '@formily/core'; import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; -import { css } from '@emotion/css'; -import { DateFormatCom } from '../../../schema-component/antd/expiresRadio'; export * as operators from './operators'; export const type: ISchema = { @@ -227,88 +225,26 @@ export const reverseFieldProperties: Record = { }; export const dateTimeProps: { [key: string]: ISchema } = { - 'uiSchema.x-component-props.picker': { - type: 'string', - title: '{{t("Picker")}}', - 'x-decorator': 'FormItem', - 'x-component': 'Radio.Group', - default: 'date', - enum: [ - { - label: '{{t("Date")}}', - value: 'date', - }, - // { - // label: '{{t("Week")}}', - // value: 'week', - // }, - { - label: '{{t("Month")}}', - value: 'month', - }, - { - label: '{{t("Quarter")}}', - value: 'quarter', - }, - { - label: '{{t("Year")}}', - value: 'year', - }, - ], - }, - 'uiSchema.x-component-props.dateFormat': { type: 'string', title: '{{t("Date format")}}', + 'x-component': 'Radio.Group', 'x-decorator': 'FormItem', - 'x-component': 'ExpiresRadio', - 'x-decorator-props': {}, - 'x-component-props': { - className: css` - .ant-radio-wrapper { - display: flex; - margin: 5px 0px; - } - `, - defaultValue: 'dddd', - formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'], - }, default: 'YYYY-MM-DD', enum: [ { - label: DateFormatCom({ format: 'MMMM Do YYYY' }), - value: 'MMMM Do YYYY', - }, - { - label: DateFormatCom({ format: 'YYYY-MM-DD' }), - value: 'YYYY-MM-DD', - }, - { - label: DateFormatCom({ format: 'MM/DD/YY' }), - value: 'MM/DD/YY', - }, - { - label: DateFormatCom({ format: 'YYYY/MM/DD' }), + label: '{{t("Year/Month/Day")}}', value: 'YYYY/MM/DD', }, { - label: DateFormatCom({ format: 'DD/MM/YYYY' }), - value: 'DD/MM/YYYY', + label: '{{t("Year-Month-Day")}}', + value: 'YYYY-MM-DD', }, { - label: 'custom', - value: 'custom', + label: '{{t("Day/Month/Year")}}', + value: 'DD/MM/YYYY', }, ], - 'x-reactions': { - dependencies: ['uiSchema.x-component-props.picker'], - fulfill: { - state: { - value: `{{ getPickerFormat($deps[0])}}`, - componentProps: { picker: `{{$deps[0]}}` }, - }, - }, - }, }, 'uiSchema.x-component-props.showTime': { type: 'boolean', @@ -322,21 +258,6 @@ export const dateTimeProps: { [key: string]: ISchema } = { f.value='HH:mm:ss' }); }}}`, - { - dependencies: ['uiSchema.x-component-props.picker'], - when: '{{$deps[0]==="date"}}', - fulfill: { - state: { - hidden: false, - }, - }, - otherwise: { - state: { - hidden: true, - value: false, - }, - }, - }, ], }, 'uiSchema.x-component-props.timeFormat': { @@ -355,14 +276,6 @@ export const dateTimeProps: { [key: string]: ISchema } = { value: 'HH:mm:ss', }, ], - 'x-reactions': { - dependencies: ['uiSchema.x-component-props.showTime'], - fulfill: { - state: { - hidden: `{{ !$deps[0] }}`, - }, - }, - }, }, }; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts index a79128316b..1e031153c0 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -60,48 +60,13 @@ export const object = [ ]; export const datetime = [ - { - label: "{{ t('is') }}", - value: '$dateOn', - selected: true, - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, //schema 仅在Filter.Action生效,筛选表单中不生效 - }, - { - label: "{{ t('is not') }}", - value: '$dateNotOn', - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, - }, - { - label: "{{ t('is before') }}", - value: '$dateBefore', - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, - }, - { - label: "{{ t('is after') }}", - value: '$dateAfter', - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, - }, - { - label: "{{ t('is on or after') }}", - value: '$dateNotBefore', - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, - }, - { - label: "{{ t('is on or before') }}", - value: '$dateNotAfter', - schema: { 'x-component': 'DatePicker.FilterWithPicker' }, - onlyFilterAction: true, - }, - { - label: "{{ t('is between') }}", - value: '$dateBetween', - schema: { 'x-component': 'DatePicker.RangePicker' }, - }, + { label: "{{ t('is') }}", value: '$dateOn', selected: true }, + { label: "{{ t('is not') }}", value: '$dateNotOn' }, + { label: "{{ t('is before') }}", value: '$dateBefore' }, + { label: "{{ t('is after') }}", value: '$dateAfter' }, + { label: "{{ t('is on or after') }}", value: '$dateNotBefore' }, + { label: "{{ t('is on or before') }}", value: '$dateNotAfter' }, + { label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } }, { label: "{{ t('is empty') }}", value: '$empty', noValue: true }, { label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true }, ]; diff --git a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx index 8166174c48..5b0de8c91a 100644 --- a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx +++ b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx @@ -8,7 +8,7 @@ */ import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { defaultProps, operators, dateTimeProps } from './properties'; +import { defaultProps, operators } from './properties'; export class UnixTimestampFieldInterface extends CollectionFieldInterface { name = 'unixTimestamp'; type = 'object'; @@ -34,7 +34,6 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface { hasDefaultValue = false; properties = { ...defaultProps, - ...dateTimeProps, accuracy: { type: 'string', title: '{{t("Accuracy")}}', diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index befd3def02..a0c616c7e3 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -106,7 +106,6 @@ export abstract class CollectionFieldInterface { 'uiSchema.x-component-props.showTime', 'uiSchema.x-component-props.dateFormat', 'uiSchema.x-component-props.timeFormat', - 'uiSchema.x-component-props.picker', ], fulfill: { state: { @@ -115,20 +114,10 @@ export abstract class CollectionFieldInterface { showTime: '{{$deps[1]}}', dateFormat: '{{$deps[2]}}', timeFormat: '{{$deps[3]}}', - picker: '{{$deps[4]}}', }, }, }, }, - { - // 当 picker 改变时,清空 defaultValue - dependencies: ['uiSchema.x-component-props.picker'], - fulfill: { - state: { - value: null, - }, - }, - }, { dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'], when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}', diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index b5f16d5eca..88dd3a3a48 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -997,8 +997,5 @@ "Are you sure you want to perform the Submit action?": "你确定执行提交操作吗?", "Perform the Trigger workflow": "执行触发工作流", "Are you sure you want to perform the Trigger workflow action?": "你确定执行触发工作流吗?", - "Ellipsis overflow content": "省略超出长度的内容", - "Picker": "选择器", - "Quarter":"季度", - "Switching the picker, the value and default value will be cleared":"切换选择器时,字段的值和默认值将会被清空" + "Ellipsis overflow content": "省略超出长度的内容" } diff --git a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx index 905b26ac7f..4e243074e8 100644 --- a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx @@ -29,21 +29,3 @@ export const datePickerComponentFieldSettings = new SchemaSettings({ }, ], }); - -export const rangePickerPickerComponentFieldSettings = new SchemaSettings({ - name: 'fieldSettings:component:DatePicker.RangePicker', - items: [ - { - name: 'dateDisplayFormat', - Component: SchemaSettingsDateFormat as any, - useComponentProps() { - const schema = useFieldSchema(); - const { fieldSchema: tableColumnSchema } = useColumnSchema(); - const fieldSchema = tableColumnSchema || schema; - return { - fieldSchema, - }; - }, - }, - ], -}); diff --git a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx index 6424155c08..02955e3314 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx @@ -7,17 +7,14 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; -import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps, Space, Select } from 'antd'; +import { connect, mapProps, mapReadPretty } from '@formily/react'; +import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps } from 'antd'; import { RangePickerProps } from 'antd/es/date-picker'; import dayjs from 'dayjs'; -import React, { useState, useContext } from 'react'; -import { getPickerFormat } from '@nocobase/utils/client'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { ReadPretty, ReadPrettyComposed } from './ReadPretty'; -import { getDateRanges, mapDatePicker, mapRangePicker, inferPickerType } from './util'; -import { FilterContext } from '../../antd/filter'; -import { useCompile } from '../../'; +import { getDateRanges, mapDatePicker, mapRangePicker } from './util'; interface IDatePickerProps { utc?: boolean; @@ -26,7 +23,6 @@ interface IDatePickerProps { type ComposedDatePicker = React.FC & { ReadPretty?: ReadPrettyComposed['DatePicker']; RangePicker?: ComposedRangePicker; - FilterWithPicker?: any; }; type ComposedRangePicker = React.FC & { @@ -64,14 +60,9 @@ export const DatePicker: ComposedDatePicker = (props: any) => { DatePicker.ReadPretty = ReadPretty.DatePicker; DatePicker.RangePicker = function RangePicker(props: any) { - const { value, picker = 'date', format } = props; const { t } = useTranslation(); - const fieldSchema = useFieldSchema(); - const field: any = useField(); const { utc = true } = useDatePickerContext(); const rangesValue = getDateRanges(); - const compile = useCompile(); - const ctx: any = useContext(FilterContext); // 在筛选按钮中使用 const presets = [ { label: t('Today'), value: rangesValue.today }, { label: t('Last week'), value: rangesValue.lastWeek }, @@ -93,138 +84,13 @@ DatePicker.RangePicker = function RangePicker(props: any) { { label: t('Last 90 days'), value: rangesValue.last90Days }, { label: t('Next 90 days'), value: rangesValue.next90Days }, ]; - - const targetPicker = value ? inferPickerType(value?.[0]) : picker; - const targetFormat = getPickerFormat(targetPicker) || format; const newProps: any = { utc, presets, ...props, - format: targetFormat, - picker: targetPicker, showTime: props.showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('00:00:00', 'HH:mm:ss')] } : false, }; - const [stateProps, setStateProps] = useState(newProps); - - if (ctx) { - return ( - - { - const format = getPickerFormat(value); - field.setComponentProps({ - picker: value, - format, - }); - newProps.picker = value; - newProps.format = format; - setStateProps(newProps); - fieldSchema['x-component-props'] = { - ...props, - picker: value, - format, - }; - field.value = null; - }} - /> - - - ); -}; - export default DatePicker; diff --git a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx index 58e1e60e22..f05fc9c312 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx @@ -34,15 +34,16 @@ export interface ReadPrettyDatePickerProps extends Str2momentOptions, GetDefault showTime?: boolean; } -ReadPretty.DatePicker = function DatePicker(props: any) { - const { value, picker = 'date' } = props; +ReadPretty.DatePicker = function DatePicker(props) { const prefixCls = usePrefixCls('description-date-picker', props); - if (!value) { + + if (!props.value) { return
; } + const getLabels = () => { const format = getDefaultFormat(props) as string; - const m = str2moment(value, props); + const m = str2moment(props.value, props); const labels = dayjs.isDayjs(m) ? m.format(format) : ''; return isArr(labels) ? labels.join('~') : labels; }; diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx index 6405a5ec1b..e89323d675 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx @@ -232,6 +232,9 @@ describe('RangePicker', () => { expect( screen.getByText(currentDateString.replace(/-/g, '/'), { selector: '.ant-description-date-picker' }), ).toBeInTheDocument(); + + // Value + expect(screen.getByText(`${currentDateString}T00:00:00.000Z`)).toBeInTheDocument(); }); }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts index 41a8feef50..f966ee3a3e 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts @@ -62,7 +62,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z'); }); it('should call onChange with correct value when showTime is true and gmt is false', () => { @@ -74,7 +74,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22 22:22:22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); + expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); }); it('should call onChange with correct value when showTime is false and gmt is true', () => { @@ -85,7 +85,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z'); }); it('should call onChange with correct value when showTime is false and gmt is false', () => { @@ -97,7 +97,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); + expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); }); it('should call onChange with correct value when picker is year and gmt is true', () => { @@ -108,7 +108,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-01-01T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); }); it('should call onChange with correct value when picker is year and gmt is false', () => { @@ -120,7 +120,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); + expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString()); }); it('should call onChange with correct value when picker is month and gmt is true', () => { @@ -131,7 +131,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z'); }); it('should call onChange with correct value when picker is month and gmt is false', () => { @@ -143,7 +143,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); + expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString()); }); it('should call onChange with correct value when picker is quarter and gmt is true', () => { @@ -154,7 +154,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); }); it('should call onChange with correct value when picker is quarter and gmt is false', () => { @@ -166,7 +166,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); + expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString()); }); it('should call onChange with correct value when picker is week and gmt is true', () => { @@ -178,7 +178,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs.utc('2022-02-21T00:00:00.000Z'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); + expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); }); it('should call onChange with correct value when picker is week and gmt is false', () => { @@ -190,7 +190,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-21 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); + expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); }); it('should call onChange with correct value when utc is false', () => { @@ -202,7 +202,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22'); }); it('should call onChange with correct value when picker is year and utc is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts index 93387abd60..5ef7d817f5 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -29,7 +29,7 @@ describe('str2moment', () => { }); test('picker is month', async () => { - const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month', gmt: true }); + const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month' }); expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-01 00:00:00'); }); }); @@ -130,10 +130,10 @@ describe('moment2str', () => { expect(str).toBe('2023-06-01T00:00:00.000Z'); }); - test('picker is month gmt is false', () => { + test('picker is month', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'month', gmt: false }); - expect(str).toBe(dayjs('2023-06-01 00:00:00').toISOString()); + const str = moment2str(m, { picker: 'month', gmt: true }); + expect(str).toBe('2023-06-01T00:00:00.000Z'); }); test('picker is week, gmt is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 9a8d2b66a3..5bb825c817 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client'; +import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; -const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => { +const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => { if (!dayjs.isDayjs(value)) return value; if (timezone === 'local') { const offset = new Date().getTimezoneOffset(); @@ -57,7 +57,7 @@ export interface Moment2strOptions { } export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => { - const { showTime, gmt, picker = 'date', utc = true } = options; + const { showTime, gmt, picker, utc = true } = options; if (!value) { return value; } @@ -74,39 +74,9 @@ export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {} return toLocalByPicker(value, picker); }; -const handleChangeOnFilter = (value, picker, showTime) => { - const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : getPickerFormat(picker); - if (value) { - return value.format(format); - } - return value; -}; - -const handleChangeOnForm = (value, dateOnly, utc, picker, showTime, gmt) => { - const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; - if (!value) { - return value; - } - if (dateOnly) { - return dayjs(value).startOf(picker).format('YYYY-MM-DD'); - } - if (utc) { - if (gmt) { - return toGmt(value); - } - if (picker !== 'date') { - return dayjs(value).startOf(picker).toISOString(); - } - const formattedDate = dayjs(value).format(format); - return dayjs(formattedDate).toISOString(); - } - return dayjs(value).startOf(picker).format(format); -}; - export const mapDatePicker = function () { return (props: any) => { - const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; - const format = getDefaultFormat(props); + const format = getDefaultFormat(props) as any; const onChange = props.onChange; return { ...props, @@ -114,10 +84,13 @@ export const mapDatePicker = function () { value: str2moment(props.value, props), onChange: (value: Dayjs | null, dateString) => { if (onChange) { - if (underFilter) { - onChange(handleChangeOnFilter(value, picker, showTime)); + if (!props.showTime && value) { + value = value.startOf('day'); + } + if (props.dateOnly) { + onChange(dateString !== '' ? dateString : undefined); } else { - onChange(handleChangeOnForm(value, dateOnly, utc, picker, showTime, gmt)); + onChange(moment2str(value, props)); } } }, @@ -129,26 +102,18 @@ export const mapRangePicker = function () { return (props: any) => { const format = getDefaultFormat(props) as any; const onChange = props.onChange; - const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; + return { ...props, format: format, value: str2moment(props.value, props), onChange: (value: Dayjs[]) => { if (onChange) { - if (underFilter) { - onChange( - value - ? [handleChangeOnFilter(value[0], picker, showTime), handleChangeOnFilter(value[1], picker, showTime)] - : [], - ); - } else { - onChange( - value - ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] - : [], - ); - } + onChange( + value + ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] + : [], + ); } }, } as any; @@ -261,17 +226,3 @@ function withParams(value: any[], params: { fieldOperator?: string }) { return value; } - -export function inferPickerType(dateString: string): 'year' | 'month' | 'quarter' | 'date' { - if (/^\d{4}$/.test(dateString)) { - return 'year'; - } else if (/^\d{4}-\d{2}$/.test(dateString)) { - return 'month'; - } else if (/^\d{4}Q[1-4]$/.test(dateString)) { - return 'quarter'; - } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { - return 'date'; - } else { - return 'date'; - } -} diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 79025dabd2..2c5062344e 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -65,7 +65,6 @@ export const DynamicComponent = (props: Props) => { ...props.style, }, utc: false, - underFilter: true, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx index a5700807d2..9143ab64f4 100644 --- a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -496,6 +496,7 @@ export const EditOperator = () => { const { dn } = useDesignable(); const operatorList = useOperatorList(); const { getOperator, collectOperator } = useOperators(); + if (operatorList.length && !getOperator(fieldSchema.name)) { collectOperator(fieldSchema.name, operatorList[0].value); } @@ -511,22 +512,22 @@ export const EditOperator = () => { _.set(fieldSchema, 'x-filter-operator', v); const operator = operatorList.find((item) => item.value === v); - let componentProps = { ...fieldSchema['x-component-props'] }; - field.value = undefined; //切换操作符清空字段值 + let componentProps = {}; + // 根据操作符的配置,设置组件的属性 - if (operator?.schema?.['x-component'] && !operator?.onlyFilterAction) { + if (operator?.schema?.['x-component']) { _.set(fieldSchema, 'x-component-props.component', operator.schema?.['x-component']); _.set(field, 'componentProps.component', operator.schema?.['x-component']); + field.reset(); componentProps = { - ...fieldSchema['x-component-props'], component: operator.schema['x-component'], ...operator.schema?.['x-component-props'], }; } else if (fieldSchema['x-component-props']?.component) { _.set(fieldSchema, 'x-component-props.component', null); _.set(field, 'componentProps.component', null); + field.reset(); componentProps = { - ...fieldSchema['x-component-props'], component: null, ...operator.schema?.['x-component-props'], }; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index 36b5d5bdbe..ba15dd74b3 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -62,6 +62,5 @@ export * from './variable'; export * from './unix-timestamp'; export * from './nanoid-input'; export * from './error-fallback'; -export * from './expiresRadio'; import './index.less'; diff --git a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx index 2ff4c514f4..05728552ac 100644 --- a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx +++ b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx @@ -11,12 +11,11 @@ import { connect, mapProps, mapReadPretty } from '@formily/react'; import React from 'react'; import ReactQuill from 'react-quill'; import { isVariable } from '../../../variables/utils/isVariable'; -import { ReadPretty as InputReadPretty, Input } from '../input'; +import { ReadPretty as InputReadPretty } from '../input'; import { useStyles } from './style'; export const RichText = connect( (props) => { - const { underFilter } = props; const { wrapSSR, hashId, componentCls } = useStyles(); const modules = { toolbar: [['bold', 'italic', 'underline', 'link'], [{ list: 'ordered' }, { list: 'bullet' }], ['clean']], @@ -36,9 +35,7 @@ export const RichText = connect( ]; const { value, defaultValue, onChange, disabled } = props; const resultValue = isVariable(value || defaultValue) ? undefined : value || defaultValue || ''; - if (underFilter) { - return ; - } + return wrapSSR( { const { value, onChange } = props; + return ( dayjs(date)); + b = b.map((date) => dayjs(date)); + + return a[0].isBetween(b[0], b[1], null, '[]') && a[1].isBetween(b[0], b[1], null, '[]'); }, $dateBefore: function (a, b) { if (!a || !b) { return false; } - // Parse both date strings - const dateA = parseDate(a); - const dateB = parseDate(b); - - if (!dateA || !dateB) { - throw new Error('Invalid date format'); + if (!Array.isArray(a)) { + a = [a, a]; } - return dateA < dateB; + if (!Array.isArray(b)) { + b = [b, b]; + } + a = a.map((date) => dayjs(date)); + b = b.map((date) => dayjs(date)); + + return a[0].isBefore(b[0]) && a[1].isBefore(b[0]); }, $dateNotBefore: function (a, b) { - if (!a || !b) { - return false; - } - const dateA = parseDate(a); - const dateB = parseDate(b); - - if (!dateA || !dateB) { - throw new Error('Invalid date format'); - } - - // Compare the two dates - return dateA >= dateB; + return !operations.$dateBefore(a, b); }, $dateAfter: function (a, b) { if (!a || !b) { return false; } - // Parse both date strings - const dateA = parseDate(a); - const dateB = parseDate(b); + if (!Array.isArray(a)) { + a = [a, a]; + } + if (!Array.isArray(b)) { + b = [b, b]; + } + a = a.map((date) => dayjs(date)); + b = b.map((date) => dayjs(date)); - return dateA > dateB; + return a[0].isAfter(b[1]) && a[1].isAfter(b[1]); }, $dateNotAfter: function (a, b) { - if (!a || !b) { - return false; - } - const dateA = parseDate(a); - const dateB = parseDate(b); - - if (!dateA || !dateB) { - throw new Error('Invalid date format'); - } - return dateA <= dateB; + return !operations.$dateAfter(a, b); }, $dateBetween: function (a, b) { - if (!a || !b) { - return false; - } - const dateA = parseFullDate(a); - const dateBStart = parseFullDate(b[0]); - const dateBEnd = parseFullDate(b[1]); - - if (!dateA || !dateBStart || !dateBEnd) { - throw new Error('Invalid date format'); - } - return dateA >= dateBStart && dateA <= dateBEnd; + return operations.$dateOn(a, b); }, $dateNotOn: function (a, b) { - if (!a || !b) { - return false; - } - return a !== b; + return !operations.$dateOn(a, b); }, $isTruly: function (a) { if (Array.isArray(a)) return a.some((k) => k === true || k === 1); @@ -632,43 +615,3 @@ export function getJsonLogic() { return jsonLogic; } - -function parseFullDate(dateStr) { - return new Date(dateStr); -} - -function parseMonth(dateStr) { - const [year, month] = dateStr.split('-').map(Number); - return new Date(year, month - 1); -} - -function parseQuarter(dateStr) { - const year = parseInt(dateStr.slice(0, 4)); - const quarter = parseInt(dateStr.slice(5, 6)); - const month = (quarter - 1) * 3; - return new Date(year, month); -} - -function parseYear(dateStr) { - const year = parseInt(dateStr); - return new Date(year, 0); -} - -function parseDate(dateStr) { - dateStr = dateStr.trim(); - - if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { - // It's in "YYYY-MM-DD" format - return parseFullDate(dateStr); - } else if (/^\d{4}-\d{2}$/.test(dateStr)) { - // It's in "YYYY-MM" format - return parseMonth(dateStr); - } else if (/^\d{4}Q[1-4]$/.test(dateStr)) { - // It's in "YYYYQn" format - return parseQuarter(dateStr); - } else if (/^\d{4}$/.test(dateStr)) { - // It's in "YYYY" format - return parseYear(dateStr); - } - return null; // Invalid format -} diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index c305551283..a0e174a3b4 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -11,13 +11,11 @@ import Handlebars from 'handlebars'; import { dayjs } from '@nocobase/utils/client'; import helpers from '@budibase/handlebars-helpers'; import _, { every, findIndex, some } from 'lodash'; -import { getPickerFormat } from '@nocobase/utils/client'; import { replaceVariableValue } from '../../../block-provider/hooks'; import { VariableOption, VariablesContextType } from '../../../variables/types'; import { isVariable } from '../../../variables/utils/isVariable'; import { transformVariableValue } from '../../../variables/utils/transformVariableValue'; import { getJsonLogic } from '../../common/utils/logic'; -import { inferPickerType } from '../../antd/date-picker/util'; import url from 'url'; type VariablesCtx = { /** 当前登录的用户 */ @@ -126,19 +124,11 @@ export const conditionAnalyses = async ({ const jsonLogic = getJsonLogic(); const [value, targetValue] = await Promise.all(parsingResult); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); - let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); - const comparisonValue = transformVariableValue(value, { targetCollectionField }); - if ( - ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) && - currentInputValue - ) { - const picker = inferPickerType(comparisonValue); - const format = getPickerFormat(picker); - currentInputValue = dayjs(currentInputValue).format(format); - } - return jsonLogic.apply({ - [operator]: [currentInputValue, comparisonValue], + [operator]: [ + transformVariableValue(targetValue, { targetCollectionField }), + transformVariableValue(value, { targetCollectionField }), + ], }); } catch (error) { throw error; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index c99cb91bf6..fd0e73825d 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -462,8 +462,8 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, 'x-component-props': { + component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'], utc: false, - underFilter: true, }, }; if (isAssocField(field)) { @@ -478,7 +478,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-decorator': 'FormItem', 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false, underFilter: true }, + 'x-component-props': field.uiSchema?.['x-component-props'], }; } const resultItem = { @@ -572,7 +572,7 @@ const associationFieldToMenu = ( interface: field.interface, }, 'x-component': 'CollectionField', - 'x-component-props': { utc: false, underFilter: true }, + 'x-component-props': { utc: false }, 'x-read-pretty': false, 'x-decorator': 'FormItem', 'x-collection-field': `${collectionName}.${schemaName}`, @@ -688,7 +688,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': { utc: false, underFilter: true }, + 'x-component-props': { utc: false }, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { diff --git a/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx b/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx similarity index 71% rename from packages/core/client/src/schema-component/antd/expiresRadio/index.tsx rename to packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx index 17b8c46fdf..7b5372353b 100644 --- a/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx +++ b/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx @@ -9,11 +9,12 @@ import { css } from '@emotion/css'; import dayjs from 'dayjs'; + import { connect, mapProps } from '@formily/react'; import { useBoolean } from 'ahooks'; import { Input, Radio, Space } from 'antd'; -import React, { useEffect, useState } from 'react'; -import { useToken } from '../../../'; +import React, { useState } from 'react'; +import { useToken } from '../../'; const date = dayjs(); @@ -23,7 +24,7 @@ const spaceCSS = css` flex: 1; } `; -const DateFormatCom = (props?) => { +export const DateFormatCom = (props?) => { const date = dayjs(); return (
@@ -52,11 +53,9 @@ const DateTimeFormatPreview = ({ content }) => { }; const InternalExpiresRadio = (props) => { - const { onChange, defaultValue, formats, picker } = props; + const { onChange, defaultValue, formats, timeFormat } = props; const [isCustom, { setFalse, setTrue }] = useBoolean(props.value && !formats.includes(props.value)); - const [targetValue, setTargetValue] = useState( - props.value && !formats.includes(props.value) ? props.value : defaultValue, - ); + const targetValue = props.value && !formats.includes(props.value) ? props.value : defaultValue; const [customFormatPreview, setCustomFormatPreview] = useState(targetValue ? date.format(targetValue) : null); const onSelectChange = (v) => { if (v.target.value === 'custom') { @@ -67,19 +66,6 @@ const InternalExpiresRadio = (props) => { onChange(v.target.value); } }; - useEffect(() => { - if (!formats.includes(props.value)) { - setTrue(); - } else { - setFalse(); - } - setTargetValue(props.value && !formats.includes(props.value) ? props.value : defaultValue); - }, [props.value]); - - useEffect(() => { - setCustomFormatPreview(targetValue ? date.format(targetValue) : null); - }, [targetValue]); - return ( @@ -90,7 +76,7 @@ const InternalExpiresRadio = (props) => { { if (e.target.value) { setCustomFormatPreview(date.format(e.target.value)); @@ -100,22 +86,19 @@ const InternalExpiresRadio = (props) => { if (isCustom) { onChange(e.target.value); } - setTargetValue(e.target.value); }} /> ); } - if (!picker || picker === 'date') { - return ( - - - {v.label} - - - ); - } + return ( + + + {v.label} + + + ); })} @@ -130,4 +113,4 @@ const ExpiresRadio = connect( }), ); -export { ExpiresRadio, DateFormatCom }; +export { ExpiresRadio }; diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx index 33d2c3379a..275c98f41b 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -11,14 +11,13 @@ import { css } from '@emotion/css'; import { ISchema, Schema, useField } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { getPickerFormat } from '@nocobase/utils/client'; import { useCollectionManager_deprecated, useDesignable } from '..'; -import { DateFormatCom, ExpiresRadio } from '../schema-component'; +import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio'; import { SchemaSettingsModalItem } from './SchemaSettings'; export const SchemaSettingsDateFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { const { fieldSchema } = props; - const field: any = useField(); + const field = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const { getCollectionJoinField } = useCollectionManager_deprecated(); @@ -32,48 +31,13 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field fieldSchema?.['x-component-props']?.timeFormat || collectionField?.uiSchema?.['x-component-props']?.timeFormat || 'HH:mm:ss'; - const pickerDefaultValue = - fieldSchema?.['x-component-props']?.picker || collectionField?.uiSchema?.['x-component-props']?.picker || 'date'; - const isReadPretty = fieldSchema['x-read-pretty'] || field.readOnly || field.readPretty; return ( { - const schema: any = { + const schema = { ['x-uid']: fieldSchema['x-uid'], }; - if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty && field.value) { - field.value = undefined; - field.initialValue = undefined; - fieldSchema.default = undefined; - schema.default = undefined; - } + console.log(field.componentProps); schema['x-component-props'] = field.componentProps || {}; fieldSchema['x-component-props'] = { ...(field.componentProps || {}), @@ -222,10 +157,6 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field const modifiedString = parts.join('.'); field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => { if (f.props.name === fieldSchema.name) { - if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty) { - f.value = undefined; - f.initialValue = undefined; - } f.setComponentProps({ ...data }); } }); diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx index 08069f4b1c..f1fe305b7e 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx @@ -110,11 +110,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: VariableInput: (inputProps) => { return ( - + ); }, diff --git a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts index f413b0c9ba..f5f2a5bf7c 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts +++ b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts @@ -48,10 +48,7 @@ import { filterFormBlockSettings } from '../modules/blocks/filter-blocks/form/fi import { filterFormItemFieldSettings } from '../modules/blocks/filter-blocks/form/filterFormItemFieldSettings'; import { markdownBlockSettings } from '../modules/blocks/other-blocks/markdown/markdownBlockSettings'; import { cascadeSelectComponentFieldSettings } from '../modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings'; -import { - datePickerComponentFieldSettings, - rangePickerPickerComponentFieldSettings, -} from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; +import { datePickerComponentFieldSettings } from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; import { fileManagerComponentFieldSettings } from '../modules/fields/component/FileManager/fileManagerComponentFieldSettings'; import { previewComponentFieldSettings } from '../modules/fields/component/FileManager/previewComponentFieldSettings'; import { uploadAttachmentComponentFieldSettings } from '../modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings'; @@ -123,7 +120,6 @@ export class SchemaSettingsPlugin extends Plugin { this.schemaSettingsManager.add(subformPopoverComponentFieldSettings); this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings); this.schemaSettingsManager.add(datePickerComponentFieldSettings); - this.schemaSettingsManager.add(rangePickerPickerComponentFieldSettings); this.schemaSettingsManager.add(unixTimestampComponentFieldSettings); this.schemaSettingsManager.add(inputNumberComponentFieldSettings); this.schemaSettingsManager.add(inputComponentSettings); diff --git a/packages/core/utils/src/date.ts b/packages/core/utils/src/date.ts index 731c23a1f9..3a7bad0485 100644 --- a/packages/core/utils/src/date.ts +++ b/packages/core/utils/src/date.ts @@ -44,7 +44,7 @@ export const getDefaultFormat = (props: GetDefaultFormatProps) => { } else if (props['picker'] === 'year') { return 'YYYY'; } else if (props['picker'] === 'week') { - return 'YYYY[W]W'; + return 'YYYY-wo'; } return props['showTime'] ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; }; @@ -68,33 +68,24 @@ export const toLocal = (value: dayjs.Dayjs) => { } }; -const convertQuarterToFirstDay = (quarterStr) => { - const year = parseInt(quarterStr.slice(0, 4)); // 提取年份 - const quarter = parseInt(quarterStr.slice(-1)); // 提取季度数字 - return dayjs().quarter(quarter).year(year); -}; - const toMoment = (val: any, options?: Str2momentOptions) => { if (!val) { return; } const offset = options.utcOffset || -1 * new Date().getTimezoneOffset(); const { gmt, picker, utc = true } = options; - if (dayjs(val).isValid()) { - if (!utc) { - return dayjs(val); - } - if (dayjs.isDayjs(val)) { - return val.utcOffset(offsetFromString(offset)); - } - if (gmt) { - return dayjs(val).utcOffset(0); - } - return dayjs(val).utcOffset(offsetFromString(offset)); - } else { - return convertQuarterToFirstDay(val); + if (!utc) { + return dayjs(val); } + + if (dayjs.isDayjs(val)) { + return val.utcOffset(offsetFromString(offset)); + } + if (gmt || picker) { + return dayjs(val).utcOffset(0); + } + return dayjs(val).utcOffset(offsetFromString(offset)); }; export const str2moment = ( @@ -207,18 +198,3 @@ function absFloor(number) { return Math.floor(number); } } - -export const getPickerFormat = (picker) => { - switch (picker) { - case 'week': - return 'YYYY[W]W'; - case 'month': - return 'YYYY-MM'; - case 'quarter': - return 'YYYY[Q]Q'; - case 'year': - return 'YYYY'; - default: - return 'YYYY-MM-DD'; - } -}; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx index 0ed93b3552..1d4c4cdfad 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx @@ -99,6 +99,7 @@ export const BulkEditField = (props: any) => { const [value, setValue] = useState(null); const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema.name) || {}; + useEffect(() => { field.value = toFormFieldValue({ [type]: value }); if (field.required) { @@ -111,12 +112,6 @@ export const BulkEditField = (props: any) => { } }, [field, type, value]); - useEffect(() => { - if (field.value === null) { - setValue(undefined); - } - }, [field.value]); - const typeChangeHandler = (val) => { setType(val); field.required = val === BulkEditFormItemValueType.ChangedTo; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx index 1cbe5476c1..03704b08c3 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx @@ -30,7 +30,6 @@ import { ResourceActionContext, useDataSourceManager, } from '@nocobase/client'; -import { getPickerFormat } from '@nocobase/utils/client'; import { message } from 'antd'; import { getCollectionSchema } from './schema/collections'; import { CollectionFields } from './CollectionFields'; @@ -209,7 +208,6 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', - getPickerFormat, }} /> diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx index 816f7c23d6..2032a92027 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx @@ -12,6 +12,7 @@ import { action } from '@formily/reactive'; import { uid } from '@formily/shared'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; +// import { CollectionFieldsTable } from '.'; import { useAPIClient, useCurrentAppInfo, @@ -28,8 +29,9 @@ import { CollectionCategoriesContext, FieldSummary, TemplateSummary, + useRequest, + useCollectionRecordData, } from '@nocobase/client'; -import { getPickerFormat } from '@nocobase/utils/client'; import { CollectionFields } from './CollectionFields'; import { collectionSchema } from './schemas/collections'; @@ -237,7 +239,6 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', - getPickerFormat, }} /> From cc0f945dbf0a048d19c2d625f35427b2060e963a Mon Sep 17 00:00:00 2001 From: chenos Date: Mon, 30 Sep 2024 18:38:08 +0800 Subject: [PATCH 66/68] feat: date type fields support setting the picker type (#5345) * refactor: date field support picker * refactor: date field support picker * refactor: date time field * refactor: date time field * refactor: locale improve * refactor: defaultValue * refactor: datetime field * refactor: remove week * fix: bug * fix: bug * fix: bug * fix: test * fix: test * fix: test * fix: test * fix: test * refactor: test * refactor: test * fix: bug * fix: bug * fix: bug * refactor: expiresRadio * refactor: datepicker * fix: bug * fix: bug * fix: bug * fix: test * refactor: change operator * refactor: change picker * refactor: datetime operator schema * refactor: filterWithPicker * refactor: support linkage rule * fix: bug * refactor: useFilterFormItemInitializerFields * refactor: useFilterFormItemInitializerFields * fix: default value for datetime * fix: bug * fix: filter date time * fix: filter date time * fix: bug * fix: bug * refactor: datePicker RangePicker * refactor: datePicker RangePicker * style: date picker style improve * fix: bug * fix: bug * fix: bug * fix: default value * fix: bulk edit datetime field * fix: picker support third party data source * fix: test * fix: bug * chore: fix unit test --------- Co-authored-by: katherinehhh Co-authored-by: Zeke Zhang <958414905@qq.com> --- .../src/block-provider/TableBlockProvider.tsx | 2 +- .../collection-manager/interfaces/dateOnly.ts | 24 +- .../interfaces/properties/index.ts | 97 +++++- .../interfaces/properties/operators.ts | 49 ++- .../interfaces/unixTimestamp.tsx | 3 +- .../CollectionFieldInterface.ts | 11 + packages/core/client/src/locale/zh-CN.json | 5 +- .../datePickerComponentFieldSettings.tsx | 18 + .../antd/date-picker/DatePicker.tsx | 141 +++++++- .../antd/date-picker/ReadPretty.tsx | 9 +- .../__tests__/date-picker.test.tsx | 3 - .../__tests__/mapDatePicker.test.ts | 26 +- .../antd/date-picker/__tests__/util.test.ts | 8 +- .../schema-component/antd/date-picker/util.ts | 81 ++++- .../antd/expiresRadio/index.tsx} | 47 ++- .../antd/filter/DynamicComponent.tsx | 1 + .../antd/form-item/SchemaSettingOptions.tsx | 11 +- .../client/src/schema-component/antd/index.ts | 1 + .../antd/rich-text/RichText.tsx | 7 +- .../__tests__/Table.settings.test.tsx | 320 ++++++++++++++++++ .../antd/unix-timestamp/UnixTimestamp.tsx | 1 - .../schema-component/common/utils/logic.js | 121 +++++-- .../schema-component/common/utils/uitls.tsx | 18 +- .../client/src/schema-initializer/utils.ts | 8 +- .../SchemaSettingsDateFormat.tsx | 77 ++++- .../SchemaSettingsDefaultValue.tsx | 6 +- .../schema-settings/SchemaSettingsPlugin.ts | 6 +- packages/core/utils/src/date.ts | 46 ++- .../src/client/component/BulkEditField.tsx | 7 +- .../CollectionsManager/ConfigurationTable.tsx | 2 + .../Configuration/ConfigurationTable.tsx | 5 +- .../src/client/manager/log/hooks.tsx | 1 - 32 files changed, 997 insertions(+), 165 deletions(-) rename packages/core/client/src/{schema-settings/DateFormat/ExpiresRadio.tsx => schema-component/antd/expiresRadio/index.tsx} (71%) create mode 100644 packages/core/client/src/schema-component/antd/table-v2/__tests__/Table.settings.test.tsx diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 0dc5506963..6e7154aacd 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -12,7 +12,7 @@ import { FormContext, useField, useFieldSchema } from '@formily/react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useCollectionManager_deprecated } from '../collection-manager'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; -import { useTableBlockParams } from '../modules/blocks/data-blocks/table'; +import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps'; import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component'; import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { useBlockHeightProps } from './hooks'; diff --git a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts index defeb80b16..7bc57cbb60 100644 --- a/packages/core/client/src/collection-manager/interfaces/dateOnly.ts +++ b/packages/core/client/src/collection-manager/interfaces/dateOnly.ts @@ -31,26 +31,12 @@ export class DateFieldInterface extends CollectionFieldInterface { hasDefaultValue = true; properties = { ...defaultProps, - 'uiSchema.x-component-props.dateFormat': { - type: 'string', - title: '{{t("Date format")}}', - 'x-component': 'Radio.Group', + ...dateTimeProps, + 'uiSchema.x-component-props.showTime': { + type: 'boolean', 'x-decorator': 'FormItem', - default: 'YYYY-MM-DD', - enum: [ - { - label: '{{t("Year/Month/Day")}}', - value: 'YYYY/MM/DD', - }, - { - label: '{{t("Year-Month-Day")}}', - value: 'YYYY-MM-DD', - }, - { - label: '{{t("Day/Month/Year")}}', - value: 'DD/MM/YYYY', - }, - ], + 'x-component': 'Checkbox', + 'x-visible': false, }, }; filterable = { diff --git a/packages/core/client/src/collection-manager/interfaces/properties/index.ts b/packages/core/client/src/collection-manager/interfaces/properties/index.ts index edb01ef255..82d08c2ad9 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/index.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/index.ts @@ -10,6 +10,8 @@ import { Field } from '@formily/core'; import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; +import { css } from '@emotion/css'; +import { DateFormatCom } from '../../../schema-component/antd/expiresRadio'; export * as operators from './operators'; export const type: ISchema = { @@ -225,26 +227,88 @@ export const reverseFieldProperties: Record = { }; export const dateTimeProps: { [key: string]: ISchema } = { + 'uiSchema.x-component-props.picker': { + type: 'string', + title: '{{t("Picker")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + default: 'date', + enum: [ + { + label: '{{t("Date")}}', + value: 'date', + }, + // { + // label: '{{t("Week")}}', + // value: 'week', + // }, + { + label: '{{t("Month")}}', + value: 'month', + }, + { + label: '{{t("Quarter")}}', + value: 'quarter', + }, + { + label: '{{t("Year")}}', + value: 'year', + }, + ], + }, + 'uiSchema.x-component-props.dateFormat': { type: 'string', title: '{{t("Date format")}}', - 'x-component': 'Radio.Group', 'x-decorator': 'FormItem', + 'x-component': 'ExpiresRadio', + 'x-decorator-props': {}, + 'x-component-props': { + className: css` + .ant-radio-wrapper { + display: flex; + margin: 5px 0px; + } + `, + defaultValue: 'dddd', + formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'], + }, default: 'YYYY-MM-DD', enum: [ { - label: '{{t("Year/Month/Day")}}', - value: 'YYYY/MM/DD', + label: DateFormatCom({ format: 'MMMM Do YYYY' }), + value: 'MMMM Do YYYY', }, { - label: '{{t("Year-Month-Day")}}', + label: DateFormatCom({ format: 'YYYY-MM-DD' }), value: 'YYYY-MM-DD', }, { - label: '{{t("Day/Month/Year")}}', + label: DateFormatCom({ format: 'MM/DD/YY' }), + value: 'MM/DD/YY', + }, + { + label: DateFormatCom({ format: 'YYYY/MM/DD' }), + value: 'YYYY/MM/DD', + }, + { + label: DateFormatCom({ format: 'DD/MM/YYYY' }), value: 'DD/MM/YYYY', }, + { + label: 'custom', + value: 'custom', + }, ], + 'x-reactions': { + dependencies: ['uiSchema.x-component-props.picker'], + fulfill: { + state: { + value: `{{ getPickerFormat($deps[0])}}`, + componentProps: { picker: `{{$deps[0]}}` }, + }, + }, + }, }, 'uiSchema.x-component-props.showTime': { type: 'boolean', @@ -258,6 +322,21 @@ export const dateTimeProps: { [key: string]: ISchema } = { f.value='HH:mm:ss' }); }}}`, + { + dependencies: ['uiSchema.x-component-props.picker'], + when: '{{$deps[0]==="date"}}', + fulfill: { + state: { + hidden: false, + }, + }, + otherwise: { + state: { + hidden: true, + value: false, + }, + }, + }, ], }, 'uiSchema.x-component-props.timeFormat': { @@ -276,6 +355,14 @@ export const dateTimeProps: { [key: string]: ISchema } = { value: 'HH:mm:ss', }, ], + 'x-reactions': { + dependencies: ['uiSchema.x-component-props.showTime'], + fulfill: { + state: { + hidden: `{{ !$deps[0] }}`, + }, + }, + }, }, }; diff --git a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts index 1e031153c0..a79128316b 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -60,13 +60,48 @@ export const object = [ ]; export const datetime = [ - { label: "{{ t('is') }}", value: '$dateOn', selected: true }, - { label: "{{ t('is not') }}", value: '$dateNotOn' }, - { label: "{{ t('is before') }}", value: '$dateBefore' }, - { label: "{{ t('is after') }}", value: '$dateAfter' }, - { label: "{{ t('is on or after') }}", value: '$dateNotBefore' }, - { label: "{{ t('is on or before') }}", value: '$dateNotAfter' }, - { label: "{{ t('is between') }}", value: '$dateBetween', schema: { 'x-component': 'DatePicker.RangePicker' } }, + { + label: "{{ t('is') }}", + value: '$dateOn', + selected: true, + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, //schema 仅在Filter.Action生效,筛选表单中不生效 + }, + { + label: "{{ t('is not') }}", + value: '$dateNotOn', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is before') }}", + value: '$dateBefore', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is after') }}", + value: '$dateAfter', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is on or after') }}", + value: '$dateNotBefore', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is on or before') }}", + value: '$dateNotAfter', + schema: { 'x-component': 'DatePicker.FilterWithPicker' }, + onlyFilterAction: true, + }, + { + label: "{{ t('is between') }}", + value: '$dateBetween', + schema: { 'x-component': 'DatePicker.RangePicker' }, + }, { label: "{{ t('is empty') }}", value: '$empty', noValue: true }, { label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true }, ]; diff --git a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx index 5b0de8c91a..8166174c48 100644 --- a/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx +++ b/packages/core/client/src/collection-manager/interfaces/unixTimestamp.tsx @@ -8,7 +8,7 @@ */ import { CollectionFieldInterface } from '../../data-source/collection-field-interface/CollectionFieldInterface'; -import { defaultProps, operators } from './properties'; +import { defaultProps, operators, dateTimeProps } from './properties'; export class UnixTimestampFieldInterface extends CollectionFieldInterface { name = 'unixTimestamp'; type = 'object'; @@ -34,6 +34,7 @@ export class UnixTimestampFieldInterface extends CollectionFieldInterface { hasDefaultValue = false; properties = { ...defaultProps, + ...dateTimeProps, accuracy: { type: 'string', title: '{{t("Accuracy")}}', diff --git a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts index a0c616c7e3..befd3def02 100644 --- a/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts +++ b/packages/core/client/src/data-source/collection-field-interface/CollectionFieldInterface.ts @@ -106,6 +106,7 @@ export abstract class CollectionFieldInterface { 'uiSchema.x-component-props.showTime', 'uiSchema.x-component-props.dateFormat', 'uiSchema.x-component-props.timeFormat', + 'uiSchema.x-component-props.picker', ], fulfill: { state: { @@ -114,10 +115,20 @@ export abstract class CollectionFieldInterface { showTime: '{{$deps[1]}}', dateFormat: '{{$deps[2]}}', timeFormat: '{{$deps[3]}}', + picker: '{{$deps[4]}}', }, }, }, }, + { + // 当 picker 改变时,清空 defaultValue + dependencies: ['uiSchema.x-component-props.picker'], + fulfill: { + state: { + value: null, + }, + }, + }, { dependencies: ['primaryKey', 'unique', 'autoIncrement', 'defaultToCurrentTime'], when: '{{$deps[0]||$deps[1]||$deps[2]||$deps[3]}}', diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 88dd3a3a48..b5f16d5eca 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -997,5 +997,8 @@ "Are you sure you want to perform the Submit action?": "你确定执行提交操作吗?", "Perform the Trigger workflow": "执行触发工作流", "Are you sure you want to perform the Trigger workflow action?": "你确定执行触发工作流吗?", - "Ellipsis overflow content": "省略超出长度的内容" + "Ellipsis overflow content": "省略超出长度的内容", + "Picker": "选择器", + "Quarter":"季度", + "Switching the picker, the value and default value will be cleared":"切换选择器时,字段的值和默认值将会被清空" } diff --git a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx index 4e243074e8..905b26ac7f 100644 --- a/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/DatePicker/datePickerComponentFieldSettings.tsx @@ -29,3 +29,21 @@ export const datePickerComponentFieldSettings = new SchemaSettings({ }, ], }); + +export const rangePickerPickerComponentFieldSettings = new SchemaSettings({ + name: 'fieldSettings:component:DatePicker.RangePicker', + items: [ + { + name: 'dateDisplayFormat', + Component: SchemaSettingsDateFormat as any, + useComponentProps() { + const schema = useFieldSchema(); + const { fieldSchema: tableColumnSchema } = useColumnSchema(); + const fieldSchema = tableColumnSchema || schema; + return { + fieldSchema, + }; + }, + }, + ], +}); diff --git a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx index 02955e3314..e78ab96885 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/DatePicker.tsx @@ -7,14 +7,16 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { connect, mapProps, mapReadPretty } from '@formily/react'; -import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps } from 'antd'; +import { connect, mapProps, mapReadPretty, useField, useFieldSchema } from '@formily/react'; +import { DatePicker as AntdDatePicker, DatePickerProps as AntdDatePickerProps, Space, Select } from 'antd'; import { RangePickerProps } from 'antd/es/date-picker'; import dayjs from 'dayjs'; -import React from 'react'; +import React, { useState } from 'react'; +import { getPickerFormat } from '@nocobase/utils/client'; import { useTranslation } from 'react-i18next'; import { ReadPretty, ReadPrettyComposed } from './ReadPretty'; -import { getDateRanges, mapDatePicker, mapRangePicker } from './util'; +import { getDateRanges, mapDatePicker, mapRangePicker, inferPickerType } from './util'; +import { useCompile } from '../../'; interface IDatePickerProps { utc?: boolean; @@ -23,6 +25,7 @@ interface IDatePickerProps { type ComposedDatePicker = React.FC & { ReadPretty?: ReadPrettyComposed['DatePicker']; RangePicker?: ComposedRangePicker; + FilterWithPicker?: any; }; type ComposedRangePicker = React.FC & { @@ -60,9 +63,14 @@ export const DatePicker: ComposedDatePicker = (props: any) => { DatePicker.ReadPretty = ReadPretty.DatePicker; DatePicker.RangePicker = function RangePicker(props: any) { + const { value, picker = 'date', format } = props; const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const field: any = useField(); const { utc = true } = useDatePickerContext(); const rangesValue = getDateRanges(); + const compile = useCompile(); + const isFilterAction: any = !fieldSchema['x-filter-operator']; // 在筛选按钮中使用 const presets = [ { label: t('Today'), value: rangesValue.today }, { label: t('Last week'), value: rangesValue.lastWeek }, @@ -84,13 +92,138 @@ DatePicker.RangePicker = function RangePicker(props: any) { { label: t('Last 90 days'), value: rangesValue.last90Days }, { label: t('Next 90 days'), value: rangesValue.next90Days }, ]; + + const targetPicker = value ? inferPickerType(value?.[0]) : picker; + const targetFormat = getPickerFormat(targetPicker) || format; const newProps: any = { utc, presets, ...props, + format: targetFormat, + picker: targetPicker, showTime: props.showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('00:00:00', 'HH:mm:ss')] } : false, }; + const [stateProps, setStateProps] = useState(newProps); + + if (isFilterAction) { + return ( + + { + const format = getPickerFormat(value); + field.setComponentProps({ + picker: value, + format, + }); + newProps.picker = value; + newProps.format = format; + setStateProps(newProps); + fieldSchema['x-component-props'] = { + ...props, + picker: value, + format, + }; + field.value = null; + }} + /> + + + ); +}; + export default DatePicker; diff --git a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx index f05fc9c312..58e1e60e22 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/ReadPretty.tsx @@ -34,16 +34,15 @@ export interface ReadPrettyDatePickerProps extends Str2momentOptions, GetDefault showTime?: boolean; } -ReadPretty.DatePicker = function DatePicker(props) { +ReadPretty.DatePicker = function DatePicker(props: any) { + const { value, picker = 'date' } = props; const prefixCls = usePrefixCls('description-date-picker', props); - - if (!props.value) { + if (!value) { return
; } - const getLabels = () => { const format = getDefaultFormat(props) as string; - const m = str2moment(props.value, props); + const m = str2moment(value, props); const labels = dayjs.isDayjs(m) ? m.format(format) : ''; return isArr(labels) ? labels.join('~') : labels; }; diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx index e89323d675..6405a5ec1b 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/date-picker.test.tsx @@ -232,9 +232,6 @@ describe('RangePicker', () => { expect( screen.getByText(currentDateString.replace(/-/g, '/'), { selector: '.ant-description-date-picker' }), ).toBeInTheDocument(); - - // Value - expect(screen.getByText(`${currentDateString}T00:00:00.000Z`)).toBeInTheDocument(); }); }); diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts index f966ee3a3e..41a8feef50 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/mapDatePicker.test.ts @@ -62,7 +62,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22T22:22:22.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when showTime is true and gmt is false', () => { @@ -74,7 +74,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22 22:22:22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when showTime is false and gmt is true', () => { @@ -85,7 +85,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); }); it('should call onChange with correct value when showTime is false and gmt is false', () => { @@ -97,7 +97,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-22'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22'); }); it('should call onChange with correct value when picker is year and gmt is true', () => { @@ -108,7 +108,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-01-01T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is year and gmt is false', () => { @@ -120,7 +120,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('year').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is month and gmt is true', () => { @@ -131,7 +131,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); }); it('should call onChange with correct value when picker is month and gmt is false', () => { @@ -143,7 +143,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('month').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-01'); }); it('should call onChange with correct value when picker is quarter and gmt is true', () => { @@ -154,7 +154,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs.utc('2022-02-22T00:00:00.000Z')); - expect(props.onChange).toHaveBeenCalledWith('2022-01-01T00:00:00.000Z'); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is quarter and gmt is false', () => { @@ -166,7 +166,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-01 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('quarter').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-01-01'); }); it('should call onChange with correct value when picker is week and gmt is true', () => { @@ -178,7 +178,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs.utc('2022-02-21T00:00:00.000Z'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); }); it('should call onChange with correct value when picker is week and gmt is false', () => { @@ -190,7 +190,7 @@ describe('mapDatePicker', () => { const result = mapDatePicker()(props); const m = dayjs('2022-02-21 00:00:00'); result.onChange(m); - expect(props.onChange).toHaveBeenCalledWith(m.startOf('week').add(1, 'day').toISOString()); + expect(props.onChange).toHaveBeenCalledWith('2022-02-20'); }); it('should call onChange with correct value when utc is false', () => { @@ -202,7 +202,7 @@ describe('mapDatePicker', () => { }; const result = mapDatePicker()(props); result.onChange(dayjs('2022-02-22 22:22:22')); - expect(props.onChange).toHaveBeenCalledWith('2022-02-22 22:22:22'); + expect(props.onChange).toHaveBeenCalledWith('2022-02-22 00:00:00'); }); it('should call onChange with correct value when picker is year and utc is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts index 5ef7d817f5..93387abd60 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/__tests__/util.test.ts @@ -29,7 +29,7 @@ describe('str2moment', () => { }); test('picker is month', async () => { - const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month' }); + const m = str2moment('2022-06-01T00:00:00.000Z', { picker: 'month', gmt: true }); expect(m.format('YYYY-MM-DD HH:mm:ss')).toBe('2022-06-01 00:00:00'); }); }); @@ -130,10 +130,10 @@ describe('moment2str', () => { expect(str).toBe('2023-06-01T00:00:00.000Z'); }); - test('picker is month', () => { + test('picker is month gmt is false', () => { const m = dayjs('2023-06-21 10:10:00'); - const str = moment2str(m, { picker: 'month', gmt: true }); - expect(str).toBe('2023-06-01T00:00:00.000Z'); + const str = moment2str(m, { picker: 'month', gmt: false }); + expect(str).toBe(dayjs('2023-06-01 00:00:00').toISOString()); }); test('picker is week, gmt is false', () => { diff --git a/packages/core/client/src/schema-component/antd/date-picker/util.ts b/packages/core/client/src/schema-component/antd/date-picker/util.ts index 5bb825c817..9a8d2b66a3 100644 --- a/packages/core/client/src/schema-component/antd/date-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/date-picker/util.ts @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { getDefaultFormat, str2moment, toGmt, toLocal } from '@nocobase/utils/client'; +import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; -const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => { +const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => { if (!dayjs.isDayjs(value)) return value; if (timezone === 'local') { const offset = new Date().getTimezoneOffset(); @@ -57,7 +57,7 @@ export interface Moment2strOptions { } export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {}) => { - const { showTime, gmt, picker, utc = true } = options; + const { showTime, gmt, picker = 'date', utc = true } = options; if (!value) { return value; } @@ -74,9 +74,39 @@ export const moment2str = (value?: Dayjs | null, options: Moment2strOptions = {} return toLocalByPicker(value, picker); }; +const handleChangeOnFilter = (value, picker, showTime) => { + const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : getPickerFormat(picker); + if (value) { + return value.format(format); + } + return value; +}; + +const handleChangeOnForm = (value, dateOnly, utc, picker, showTime, gmt) => { + const format = showTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; + if (!value) { + return value; + } + if (dateOnly) { + return dayjs(value).startOf(picker).format('YYYY-MM-DD'); + } + if (utc) { + if (gmt) { + return toGmt(value); + } + if (picker !== 'date') { + return dayjs(value).startOf(picker).toISOString(); + } + const formattedDate = dayjs(value).format(format); + return dayjs(formattedDate).toISOString(); + } + return dayjs(value).startOf(picker).format(format); +}; + export const mapDatePicker = function () { return (props: any) => { - const format = getDefaultFormat(props) as any; + const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; + const format = getDefaultFormat(props); const onChange = props.onChange; return { ...props, @@ -84,13 +114,10 @@ export const mapDatePicker = function () { value: str2moment(props.value, props), onChange: (value: Dayjs | null, dateString) => { if (onChange) { - if (!props.showTime && value) { - value = value.startOf('day'); - } - if (props.dateOnly) { - onChange(dateString !== '' ? dateString : undefined); + if (underFilter) { + onChange(handleChangeOnFilter(value, picker, showTime)); } else { - onChange(moment2str(value, props)); + onChange(handleChangeOnForm(value, dateOnly, utc, picker, showTime, gmt)); } } }, @@ -102,18 +129,26 @@ export const mapRangePicker = function () { return (props: any) => { const format = getDefaultFormat(props) as any; const onChange = props.onChange; - + const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; return { ...props, format: format, value: str2moment(props.value, props), onChange: (value: Dayjs[]) => { if (onChange) { - onChange( - value - ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] - : [], - ); + if (underFilter) { + onChange( + value + ? [handleChangeOnFilter(value[0], picker, showTime), handleChangeOnFilter(value[1], picker, showTime)] + : [], + ); + } else { + onChange( + value + ? [moment2str(getRangeStart(value[0], props), props), moment2str(getRangeEnd(value[1], props), props)] + : [], + ); + } } }, } as any; @@ -226,3 +261,17 @@ function withParams(value: any[], params: { fieldOperator?: string }) { return value; } + +export function inferPickerType(dateString: string): 'year' | 'month' | 'quarter' | 'date' { + if (/^\d{4}$/.test(dateString)) { + return 'year'; + } else if (/^\d{4}-\d{2}$/.test(dateString)) { + return 'month'; + } else if (/^\d{4}Q[1-4]$/.test(dateString)) { + return 'quarter'; + } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { + return 'date'; + } else { + return 'date'; + } +} diff --git a/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx b/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx similarity index 71% rename from packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx rename to packages/core/client/src/schema-component/antd/expiresRadio/index.tsx index 7b5372353b..17b8c46fdf 100644 --- a/packages/core/client/src/schema-settings/DateFormat/ExpiresRadio.tsx +++ b/packages/core/client/src/schema-component/antd/expiresRadio/index.tsx @@ -9,12 +9,11 @@ import { css } from '@emotion/css'; import dayjs from 'dayjs'; - import { connect, mapProps } from '@formily/react'; import { useBoolean } from 'ahooks'; import { Input, Radio, Space } from 'antd'; -import React, { useState } from 'react'; -import { useToken } from '../../'; +import React, { useEffect, useState } from 'react'; +import { useToken } from '../../../'; const date = dayjs(); @@ -24,7 +23,7 @@ const spaceCSS = css` flex: 1; } `; -export const DateFormatCom = (props?) => { +const DateFormatCom = (props?) => { const date = dayjs(); return (
@@ -53,9 +52,11 @@ const DateTimeFormatPreview = ({ content }) => { }; const InternalExpiresRadio = (props) => { - const { onChange, defaultValue, formats, timeFormat } = props; + const { onChange, defaultValue, formats, picker } = props; const [isCustom, { setFalse, setTrue }] = useBoolean(props.value && !formats.includes(props.value)); - const targetValue = props.value && !formats.includes(props.value) ? props.value : defaultValue; + const [targetValue, setTargetValue] = useState( + props.value && !formats.includes(props.value) ? props.value : defaultValue, + ); const [customFormatPreview, setCustomFormatPreview] = useState(targetValue ? date.format(targetValue) : null); const onSelectChange = (v) => { if (v.target.value === 'custom') { @@ -66,6 +67,19 @@ const InternalExpiresRadio = (props) => { onChange(v.target.value); } }; + useEffect(() => { + if (!formats.includes(props.value)) { + setTrue(); + } else { + setFalse(); + } + setTargetValue(props.value && !formats.includes(props.value) ? props.value : defaultValue); + }, [props.value]); + + useEffect(() => { + setCustomFormatPreview(targetValue ? date.format(targetValue) : null); + }, [targetValue]); + return ( @@ -76,7 +90,7 @@ const InternalExpiresRadio = (props) => { { if (e.target.value) { setCustomFormatPreview(date.format(e.target.value)); @@ -86,19 +100,22 @@ const InternalExpiresRadio = (props) => { if (isCustom) { onChange(e.target.value); } + setTargetValue(e.target.value); }} /> ); } - return ( - - - {v.label} - - - ); + if (!picker || picker === 'date') { + return ( + + + {v.label} + + + ); + } })} @@ -113,4 +130,4 @@ const ExpiresRadio = connect( }), ); -export { ExpiresRadio }; +export { ExpiresRadio, DateFormatCom }; diff --git a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx index 2c5062344e..79025dabd2 100644 --- a/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx +++ b/packages/core/client/src/schema-component/antd/filter/DynamicComponent.tsx @@ -65,6 +65,7 @@ export const DynamicComponent = (props: Props) => { ...props.style, }, utc: false, + underFilter: true, }), name: 'value', 'x-read-pretty': false, diff --git a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx index 9143ab64f4..a5700807d2 100644 --- a/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/SchemaSettingOptions.tsx @@ -496,7 +496,6 @@ export const EditOperator = () => { const { dn } = useDesignable(); const operatorList = useOperatorList(); const { getOperator, collectOperator } = useOperators(); - if (operatorList.length && !getOperator(fieldSchema.name)) { collectOperator(fieldSchema.name, operatorList[0].value); } @@ -512,22 +511,22 @@ export const EditOperator = () => { _.set(fieldSchema, 'x-filter-operator', v); const operator = operatorList.find((item) => item.value === v); - let componentProps = {}; - + let componentProps = { ...fieldSchema['x-component-props'] }; + field.value = undefined; //切换操作符清空字段值 // 根据操作符的配置,设置组件的属性 - if (operator?.schema?.['x-component']) { + if (operator?.schema?.['x-component'] && !operator?.onlyFilterAction) { _.set(fieldSchema, 'x-component-props.component', operator.schema?.['x-component']); _.set(field, 'componentProps.component', operator.schema?.['x-component']); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: operator.schema['x-component'], ...operator.schema?.['x-component-props'], }; } else if (fieldSchema['x-component-props']?.component) { _.set(fieldSchema, 'x-component-props.component', null); _.set(field, 'componentProps.component', null); - field.reset(); componentProps = { + ...fieldSchema['x-component-props'], component: null, ...operator.schema?.['x-component-props'], }; diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index ba15dd74b3..36b5d5bdbe 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -62,5 +62,6 @@ export * from './variable'; export * from './unix-timestamp'; export * from './nanoid-input'; export * from './error-fallback'; +export * from './expiresRadio'; import './index.less'; diff --git a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx index 05728552ac..2ff4c514f4 100644 --- a/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx +++ b/packages/core/client/src/schema-component/antd/rich-text/RichText.tsx @@ -11,11 +11,12 @@ import { connect, mapProps, mapReadPretty } from '@formily/react'; import React from 'react'; import ReactQuill from 'react-quill'; import { isVariable } from '../../../variables/utils/isVariable'; -import { ReadPretty as InputReadPretty } from '../input'; +import { ReadPretty as InputReadPretty, Input } from '../input'; import { useStyles } from './style'; export const RichText = connect( (props) => { + const { underFilter } = props; const { wrapSSR, hashId, componentCls } = useStyles(); const modules = { toolbar: [['bold', 'italic', 'underline', 'link'], [{ list: 'ordered' }, { list: 'bullet' }], ['clean']], @@ -35,7 +36,9 @@ export const RichText = connect( ]; const { value, defaultValue, onChange, disabled } = props; const resultValue = isVariable(value || defaultValue) ? undefined : value || defaultValue || ''; - + if (underFilter) { + return ; + } return wrapSSR( { + const TableBlockProviderWithSchema = withSchema(TableBlockProvider); + + const checkTableSettings = (more: CheckSettingsOptions[] = []) => { + return checkSettings( + [ + { + title: 'Edit block title', + type: 'modal', + }, + { + title: 'Set block height', + type: 'modal', + }, + { + title: 'Enable drag and drop sorting', + type: 'switch', + async afterFirstClick() { + await checkSchema({ + 'x-decorator-props': { + dragSort: true, + }, + }); + expect(screen.queryByText('Drag and drop sorting field')).toBeInTheDocument(); + + await checkSettings([ + { + title: 'Drag and drop sorting field', + type: 'select', + options: [ + { + label: 'sort', + async checker() { + await checkSchema({ + 'x-decorator-props': { + dragSortBy: 'sortName', + }, + }); + }, + }, + ], + }, + ]); + }, + async afterSecondClick() { + await checkSchema({ + 'x-decorator-props': { + dragSort: false, + }, + }); + expect(screen.queryByText('Drag and drop sorting field')).not.toBeInTheDocument(); + }, + }, + // { + // title: 'Fix block', + // type: 'switch', + // async afterFirstClick() { + // await checkSchema({ + // 'x-decorator-props': { + // fixedBlock: true, + // }, + // }); + // }, + // async afterSecondClick() { + // await checkSchema({ + // 'x-decorator-props': { + // fixedBlock: false, + // }, + // }); + // }, + // }, + { + title: 'Set the data scope', + type: 'modal', + modalChecker: { + modalTitle: 'Set the data scope', + async beforeCheck() { + await userEvent.click(screen.getByText('Add condition')); + + await waitFor(() => { + expect(screen.queryByTestId('select-filter-field')).toBeInTheDocument(); + }); + + const field = screen.queryByTestId('select-filter-field').querySelector('input'); + + await userEvent.click(field); + await waitFor(() => { + expect(screen.queryByTitle('ID')).toBeInTheDocument(); + }); + await userEvent.click(screen.getByTitle('ID')); + + const value = document.querySelector('input[role=spinbutton]'); + await userEvent.type(value, '1'); + + await waitFor(() => { + expect(document.querySelector('input[role=spinbutton]')).toHaveValue('1'); + }); + }, + async afterSubmit() { + await checkSchema({ + 'x-decorator-props': { + params: { + filter: { + $and: [ + { + id: { + $eq: 1, + }, + }, + ], + }, + }, + }, + }); + }, + }, + }, + { + title: 'Set default sorting rules', + type: 'modal', + modalChecker: { + modalTitle: 'Set default sorting rules', + contentText: 'Add sort field', + async beforeCheck() { + await userEvent.click(screen.getByText('Add sort field')); + const dialog = screen.getByRole('dialog'); + await waitFor(() => { + expect(dialog.querySelector('.ant-select-selector')).toBeInTheDocument(); + }); + await userEvent.click(dialog.querySelector('.ant-select-selector')); + await waitFor(() => { + expect(screen.queryByText('ID')).toBeInTheDocument(); + }); + await userEvent.click(screen.getByText('ID')); + + await userEvent.click(screen.getByText('DESC')); + }, + async afterSubmit() { + await checkSchema({ + 'x-decorator-props': { + params: { + sort: ['-id'], + }, + }, + }); + }, + }, + }, + { + title: 'Set data loading mode', + type: 'modal', + modalChecker: { + modalTitle: 'Data loading mode', + async beforeCheck() { + await userEvent.click(screen.getByText('Do not load data when filter is empty')); + }, + async afterSubmit() { + await checkSchema({ + 'x-decorator-props': { + dataLoadingMode: 'manual', + }, + }); + }, + }, + }, + { + title: 'Records per page', + type: 'select', + options: [ + { + label: '10', + async checker() { + await checkSchema({ + 'x-decorator-props': { + params: { + pageSize: 10, + }, + }, + }); + }, + }, + { + label: '20', + }, + { + label: '50', + }, + { + label: '100', + }, + { + label: '100', + }, + ], + }, + { + title: 'Save as template', + type: 'modal', + }, + { + title: 'Delete', + type: 'delete', + }, + ...more, + ], + true, + ); + }; + + const getRenderSettingsOptions = (isOld?: boolean, collection = 'users') => { + const toolbarSchema = isOld + ? { + 'x-designer': 'TableBlockDesigner', + } + : { + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + }; + + return { + designable: true, + enableUserListDataBlock: true, + schema: { + type: 'void', + 'x-component': 'FixedBlock', + properties: { + table: { + type: 'void', + 'x-decorator': 'TableBlockProviderWithSchema', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: collection, + dataSource: 'main', + action: 'list', + rowKey: 'id', + showIndex: true, + dragSort: false, + params: { + pageSize: 20, + }, + }, + ...toolbarSchema, + 'x-component': 'CardItem', + 'x-index': 1, + }, + }, + }, + appOptions: { + components: { + TableBlockProviderWithSchema, + FixedBlock, + }, + plugins: [BlockSchemaComponentPlugin], + scopes: { + useTableBlockDecoratorProps, + }, + }, + }; + }; + + test('menu list', async () => { + await renderSettings(getRenderSettingsOptions()); + await checkTableSettings(); + }); + + test('old schema', async () => { + await renderSettings(getRenderSettingsOptions(true)); + await checkTableSettings(); + }); + + test('tree collection', async () => { + await renderSettings(getRenderSettingsOptions(false, 'tree')); + await checkSettings([ + { + title: 'Tree table', + type: 'switch', + async afterFirstClick() { + await checkSchema({ + 'x-decorator-props': { + treeTable: true, + }, + }); + }, + async afterSecondClick() { + await checkSchema({ + 'x-decorator-props': { + treeTable: false, + }, + }); + }, + }, + ]); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx index e83f9d00bc..d461338686 100644 --- a/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx +++ b/packages/core/client/src/schema-component/antd/unix-timestamp/UnixTimestamp.tsx @@ -19,7 +19,6 @@ interface UnixTimestampProps { export const UnixTimestamp = connect( (props: UnixTimestampProps) => { const { value, onChange } = props; - return ( dayjs(date)); - b = b.map((date) => dayjs(date)); - - return a[0].isBetween(b[0], b[1], null, '[]') && a[1].isBetween(b[0], b[1], null, '[]'); + return a === b; }, $dateBefore: function (a, b) { if (!a || !b) { return false; } - if (!Array.isArray(a)) { - a = [a, a]; - } - if (!Array.isArray(b)) { - b = [b, b]; - } - a = a.map((date) => dayjs(date)); - b = b.map((date) => dayjs(date)); + // Parse both date strings + const dateA = parseDate(a); + const dateB = parseDate(b); - return a[0].isBefore(b[0]) && a[1].isBefore(b[0]); + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + return dateA < dateB; }, $dateNotBefore: function (a, b) { - return !operations.$dateBefore(a, b); + if (!a || !b) { + return false; + } + const dateA = parseDate(a); + const dateB = parseDate(b); + + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + + // Compare the two dates + return dateA >= dateB; }, $dateAfter: function (a, b) { if (!a || !b) { return false; } - if (!Array.isArray(a)) { - a = [a, a]; - } - if (!Array.isArray(b)) { - b = [b, b]; - } - a = a.map((date) => dayjs(date)); - b = b.map((date) => dayjs(date)); + // Parse both date strings + const dateA = parseDate(a); + const dateB = parseDate(b); - return a[0].isAfter(b[1]) && a[1].isAfter(b[1]); + return dateA > dateB; }, $dateNotAfter: function (a, b) { - return !operations.$dateAfter(a, b); + if (!a || !b) { + return false; + } + const dateA = parseDate(a); + const dateB = parseDate(b); + + if (!dateA || !dateB) { + throw new Error('Invalid date format'); + } + return dateA <= dateB; }, $dateBetween: function (a, b) { - return operations.$dateOn(a, b); + if (!a || !b) { + return false; + } + const dateA = parseFullDate(a); + const dateBStart = parseFullDate(b[0]); + const dateBEnd = parseFullDate(b[1]); + + if (!dateA || !dateBStart || !dateBEnd) { + throw new Error('Invalid date format'); + } + return dateA >= dateBStart && dateA <= dateBEnd; }, $dateNotOn: function (a, b) { - return !operations.$dateOn(a, b); + if (!a || !b) { + return false; + } + return a !== b; }, $isTruly: function (a) { if (Array.isArray(a)) return a.some((k) => k === true || k === 1); @@ -615,3 +632,43 @@ export function getJsonLogic() { return jsonLogic; } + +function parseFullDate(dateStr) { + return new Date(dateStr); +} + +function parseMonth(dateStr) { + const [year, month] = dateStr.split('-').map(Number); + return new Date(year, month - 1); +} + +function parseQuarter(dateStr) { + const year = parseInt(dateStr.slice(0, 4)); + const quarter = parseInt(dateStr.slice(5, 6)); + const month = (quarter - 1) * 3; + return new Date(year, month); +} + +function parseYear(dateStr) { + const year = parseInt(dateStr); + return new Date(year, 0); +} + +function parseDate(dateStr) { + dateStr = dateStr.trim(); + + if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { + // It's in "YYYY-MM-DD" format + return parseFullDate(dateStr); + } else if (/^\d{4}-\d{2}$/.test(dateStr)) { + // It's in "YYYY-MM" format + return parseMonth(dateStr); + } else if (/^\d{4}Q[1-4]$/.test(dateStr)) { + // It's in "YYYYQn" format + return parseQuarter(dateStr); + } else if (/^\d{4}$/.test(dateStr)) { + // It's in "YYYY" format + return parseYear(dateStr); + } + return null; // Invalid format +} diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index a0e174a3b4..c305551283 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -11,11 +11,13 @@ import Handlebars from 'handlebars'; import { dayjs } from '@nocobase/utils/client'; import helpers from '@budibase/handlebars-helpers'; import _, { every, findIndex, some } from 'lodash'; +import { getPickerFormat } from '@nocobase/utils/client'; import { replaceVariableValue } from '../../../block-provider/hooks'; import { VariableOption, VariablesContextType } from '../../../variables/types'; import { isVariable } from '../../../variables/utils/isVariable'; import { transformVariableValue } from '../../../variables/utils/transformVariableValue'; import { getJsonLogic } from '../../common/utils/logic'; +import { inferPickerType } from '../../antd/date-picker/util'; import url from 'url'; type VariablesCtx = { /** 当前登录的用户 */ @@ -124,11 +126,19 @@ export const conditionAnalyses = async ({ const jsonLogic = getJsonLogic(); const [value, targetValue] = await Promise.all(parsingResult); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); + let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); + const comparisonValue = transformVariableValue(value, { targetCollectionField }); + if ( + ['datetime', 'date', 'datetimeNoTz', 'dateOnly', 'unixTimestamp'].includes(targetCollectionField.type) && + currentInputValue + ) { + const picker = inferPickerType(comparisonValue); + const format = getPickerFormat(picker); + currentInputValue = dayjs(currentInputValue).format(format); + } + return jsonLogic.apply({ - [operator]: [ - transformVariableValue(targetValue, { targetCollectionField }), - transformVariableValue(value, { targetCollectionField }), - ], + [operator]: [currentInputValue, comparisonValue], }); } catch (error) { throw error; diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index 25072b2127..eb1d74105b 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -462,8 +462,8 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, 'x-component-props': { - component: interfaceConfig?.filterable?.operators?.[0]?.schema?.['x-component'], utc: false, + underFilter: true, }, }; if (isAssocField(field)) { @@ -478,7 +478,7 @@ export const useFilterFormItemInitializerFields = (options?: any) => { 'x-decorator': 'FormItem', 'x-use-decorator-props': 'useFormItemProps', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': field.uiSchema?.['x-component-props'], + 'x-component-props': { ...field.uiSchema?.['x-component-props'], utc: false, underFilter: true }, }; } const resultItem = { @@ -572,7 +572,7 @@ const associationFieldToMenu = ( interface: field.interface, }, 'x-component': 'CollectionField', - 'x-component-props': { utc: false }, + 'x-component-props': { utc: false, underFilter: true }, 'x-read-pretty': false, 'x-decorator': 'FormItem', 'x-collection-field': `${collectionName}.${schemaName}`, @@ -688,7 +688,7 @@ export const useFilterInheritsFormItemInitializerFields = (options?) => { 'x-component': 'CollectionField', 'x-decorator': 'FormItem', 'x-collection-field': `${name}.${field.name}`, - 'x-component-props': { utc: false }, + 'x-component-props': { utc: false, underFilter: true }, 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], }; return { diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx index 275c98f41b..33d2c3379a 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDateFormat.tsx @@ -11,13 +11,14 @@ import { css } from '@emotion/css'; import { ISchema, Schema, useField } from '@formily/react'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { getPickerFormat } from '@nocobase/utils/client'; import { useCollectionManager_deprecated, useDesignable } from '..'; -import { DateFormatCom, ExpiresRadio } from './DateFormat/ExpiresRadio'; +import { DateFormatCom, ExpiresRadio } from '../schema-component'; import { SchemaSettingsModalItem } from './SchemaSettings'; export const SchemaSettingsDateFormat = function DateFormatConfig(props: { fieldSchema: Schema }) { const { fieldSchema } = props; - const field = useField(); + const field: any = useField(); const { dn } = useDesignable(); const { t } = useTranslation(); const { getCollectionJoinField } = useCollectionManager_deprecated(); @@ -31,13 +32,48 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field fieldSchema?.['x-component-props']?.timeFormat || collectionField?.uiSchema?.['x-component-props']?.timeFormat || 'HH:mm:ss'; + const pickerDefaultValue = + fieldSchema?.['x-component-props']?.picker || collectionField?.uiSchema?.['x-component-props']?.picker || 'date'; + const isReadPretty = fieldSchema['x-read-pretty'] || field.readOnly || field.readPretty; return ( { - const schema = { + const schema: any = { ['x-uid']: fieldSchema['x-uid'], }; - console.log(field.componentProps); + if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty && field.value) { + field.value = undefined; + field.initialValue = undefined; + fieldSchema.default = undefined; + schema.default = undefined; + } schema['x-component-props'] = field.componentProps || {}; fieldSchema['x-component-props'] = { ...(field.componentProps || {}), @@ -157,6 +222,10 @@ export const SchemaSettingsDateFormat = function DateFormatConfig(props: { field const modifiedString = parts.join('.'); field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => { if (f.props.name === fieldSchema.name) { + if ((field.componentProps.picker || 'date') !== data.picker && !isReadPretty) { + f.value = undefined; + f.initialValue = undefined; + } f.setComponentProps({ ...data }); } }); diff --git a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx index f1fe305b7e..08069f4b1c 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsDefaultValue.tsx @@ -110,7 +110,11 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: VariableInput: (inputProps) => { return ( - + ); }, diff --git a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts index f5f2a5bf7c..f413b0c9ba 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts +++ b/packages/core/client/src/schema-settings/SchemaSettingsPlugin.ts @@ -48,7 +48,10 @@ import { filterFormBlockSettings } from '../modules/blocks/filter-blocks/form/fi import { filterFormItemFieldSettings } from '../modules/blocks/filter-blocks/form/filterFormItemFieldSettings'; import { markdownBlockSettings } from '../modules/blocks/other-blocks/markdown/markdownBlockSettings'; import { cascadeSelectComponentFieldSettings } from '../modules/fields/component/CascadeSelect/cascadeSelectComponentFieldSettings'; -import { datePickerComponentFieldSettings } from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; +import { + datePickerComponentFieldSettings, + rangePickerPickerComponentFieldSettings, +} from '../modules/fields/component/DatePicker/datePickerComponentFieldSettings'; import { fileManagerComponentFieldSettings } from '../modules/fields/component/FileManager/fileManagerComponentFieldSettings'; import { previewComponentFieldSettings } from '../modules/fields/component/FileManager/previewComponentFieldSettings'; import { uploadAttachmentComponentFieldSettings } from '../modules/fields/component/FileManager/uploadAttachmentComponentFieldSettings'; @@ -120,6 +123,7 @@ export class SchemaSettingsPlugin extends Plugin { this.schemaSettingsManager.add(subformPopoverComponentFieldSettings); this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings); this.schemaSettingsManager.add(datePickerComponentFieldSettings); + this.schemaSettingsManager.add(rangePickerPickerComponentFieldSettings); this.schemaSettingsManager.add(unixTimestampComponentFieldSettings); this.schemaSettingsManager.add(inputNumberComponentFieldSettings); this.schemaSettingsManager.add(inputComponentSettings); diff --git a/packages/core/utils/src/date.ts b/packages/core/utils/src/date.ts index 3a7bad0485..731c23a1f9 100644 --- a/packages/core/utils/src/date.ts +++ b/packages/core/utils/src/date.ts @@ -44,7 +44,7 @@ export const getDefaultFormat = (props: GetDefaultFormatProps) => { } else if (props['picker'] === 'year') { return 'YYYY'; } else if (props['picker'] === 'week') { - return 'YYYY-wo'; + return 'YYYY[W]W'; } return props['showTime'] ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; }; @@ -68,24 +68,33 @@ export const toLocal = (value: dayjs.Dayjs) => { } }; +const convertQuarterToFirstDay = (quarterStr) => { + const year = parseInt(quarterStr.slice(0, 4)); // 提取年份 + const quarter = parseInt(quarterStr.slice(-1)); // 提取季度数字 + return dayjs().quarter(quarter).year(year); +}; + const toMoment = (val: any, options?: Str2momentOptions) => { if (!val) { return; } const offset = options.utcOffset || -1 * new Date().getTimezoneOffset(); const { gmt, picker, utc = true } = options; + if (dayjs(val).isValid()) { + if (!utc) { + return dayjs(val); + } - if (!utc) { - return dayjs(val); + if (dayjs.isDayjs(val)) { + return val.utcOffset(offsetFromString(offset)); + } + if (gmt) { + return dayjs(val).utcOffset(0); + } + return dayjs(val).utcOffset(offsetFromString(offset)); + } else { + return convertQuarterToFirstDay(val); } - - if (dayjs.isDayjs(val)) { - return val.utcOffset(offsetFromString(offset)); - } - if (gmt || picker) { - return dayjs(val).utcOffset(0); - } - return dayjs(val).utcOffset(offsetFromString(offset)); }; export const str2moment = ( @@ -198,3 +207,18 @@ function absFloor(number) { return Math.floor(number); } } + +export const getPickerFormat = (picker) => { + switch (picker) { + case 'week': + return 'YYYY[W]W'; + case 'month': + return 'YYYY-MM'; + case 'quarter': + return 'YYYY[Q]Q'; + case 'year': + return 'YYYY'; + default: + return 'YYYY-MM-DD'; + } +}; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx index 1d4c4cdfad..0ed93b3552 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/component/BulkEditField.tsx @@ -99,7 +99,6 @@ export const BulkEditField = (props: any) => { const [value, setValue] = useState(null); const { getField } = useCollection_deprecated(); const collectionField = getField(fieldSchema.name) || {}; - useEffect(() => { field.value = toFormFieldValue({ [type]: value }); if (field.required) { @@ -112,6 +111,12 @@ export const BulkEditField = (props: any) => { } }, [field, type, value]); + useEffect(() => { + if (field.value === null) { + setValue(undefined); + } + }, [field.value]); + const typeChangeHandler = (val) => { setType(val); field.required = val === BulkEditFormItemValueType.ChangedTo; diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx index 03704b08c3..1cbe5476c1 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/CollectionsManager/ConfigurationTable.tsx @@ -30,6 +30,7 @@ import { ResourceActionContext, useDataSourceManager, } from '@nocobase/client'; +import { getPickerFormat } from '@nocobase/utils/client'; import { message } from 'antd'; import { getCollectionSchema } from './schema/collections'; import { CollectionFields } from './CollectionFields'; @@ -208,6 +209,7 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', + getPickerFormat, }} /> diff --git a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx index 2032a92027..816f7c23d6 100644 --- a/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx +++ b/packages/plugins/@nocobase/plugin-data-source-manager/src/client/component/MainDataSourceManager/Configuration/ConfigurationTable.tsx @@ -12,7 +12,6 @@ import { action } from '@formily/reactive'; import { uid } from '@formily/shared'; import React, { useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; -// import { CollectionFieldsTable } from '.'; import { useAPIClient, useCurrentAppInfo, @@ -29,9 +28,8 @@ import { CollectionCategoriesContext, FieldSummary, TemplateSummary, - useRequest, - useCollectionRecordData, } from '@nocobase/client'; +import { getPickerFormat } from '@nocobase/utils/client'; import { CollectionFields } from './CollectionFields'; import { collectionSchema } from './schemas/collections'; @@ -239,6 +237,7 @@ export const ConfigurationTable = () => { interfaces, enableInherits: database?.dialect === 'postgres', isPG: database?.dialect === 'postgres', + getPickerFormat, }} /> diff --git a/packages/plugins/@nocobase/plugin-notification-manager/src/client/manager/log/hooks.tsx b/packages/plugins/@nocobase/plugin-notification-manager/src/client/manager/log/hooks.tsx index d3436f53a5..f3eb30f912 100644 --- a/packages/plugins/@nocobase/plugin-notification-manager/src/client/manager/log/hooks.tsx +++ b/packages/plugins/@nocobase/plugin-notification-manager/src/client/manager/log/hooks.tsx @@ -6,4 +6,3 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ - From 7ad4d8db1876e73dc5226e0bbd8085c35699acbe Mon Sep 17 00:00:00 2001 From: gchust Date: Mon, 30 Sep 2024 19:57:04 +0800 Subject: [PATCH 67/68] fix: incorrect pg_restore in pro image (#5347) * fix: incorrect pg_restore in pro image * chore: update comment --- Dockerfile.pro | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/Dockerfile.pro b/Dockerfile.pro index dd229f7c85..4a01713c2f 100644 --- a/Dockerfile.pro +++ b/Dockerfile.pro @@ -4,7 +4,7 @@ ARG COMMIT_HASH ARG APPEND_PRESET_LOCAL_PLUGINS ARG BEFORE_PACK_NOCOBASE="ls -l" ARG PLUGINS_DIRS -ARG PG_CLIENT_VERSION="16.1" +ARG PG_CLIENT_VERSION=16 ENV PLUGINS_DIRS=${PLUGINS_DIRS} @@ -41,30 +41,19 @@ RUN cd /app \ && rm -rf nocobase.tar.gz \ && tar -zcf ./nocobase.tar.gz -C /app/my-nocobase-app . -# add database client -FROM debian:11-slim AS dbclient-builder -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - wget \ - dpkg \ - ca-certificates \ - build-essential \ - libreadline-dev \ - zlib1g-dev \ - libicu-dev - -RUN wget https://ftp.postgresql.org/pub/source/v16.0/postgresql-16.0.tar.gz && \ - tar -xzf postgresql-16.0.tar.gz -WORKDIR /postgresql-16.0 -RUN ./configure --without-readline --without-zlib --without-icu && \ - make -C src/bin/pg_dump -RUN wget https://repo.mysql.com/apt/debian/pool/mysql-8.0/m/mysql-community/mysql-community-client-core_8.0.37-1debian11_amd64.deb && \ - dpkg-deb -x mysql-community-client-core_8.0.37-1debian11_amd64.deb /tmp/mysql-community-client - FROM node:20.13-bullseye-slim -RUN apt-get update && apt-get install -y nginx libpq5 libreadline8 +RUN apt-get update && apt-get install -y nginx RUN rm -rf /etc/nginx/sites-enabled/default + +# install postgresql-client and mysql-client +RUN apt update && apt install -y wget postgresql-common gnupg \ + && /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \ + && apt install -y postgresql-client-${PG_CLIENT_VERSION} \ + && wget https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client-core_8.1.0-1debian11_amd64.deb \ + && dpkg -x mysql-community-client-core_8.1.0-1debian11_amd64.deb /tmp/mysql-client \ + && cp /tmp/mysql-client/usr/bin/mysqldump /usr/bin/ \ + && cp /tmp/mysql-client/usr/bin/mysql /usr/bin/ + COPY ./docker/nocobase/nocobase.conf /etc/nginx/sites-enabled/nocobase.conf COPY --from=builder /app/nocobase.tar.gz /app/nocobase.tar.gz @@ -72,11 +61,6 @@ WORKDIR /app/nocobase RUN mkdir -p /app/nocobase/storage/uploads/ && echo "$COMMIT_HASH" >> /app/nocobase/storage/uploads/COMMIT_HASH -COPY --from=dbclient-builder /postgresql-16.0/src/bin/pg_dump/pg_dump /usr/local/bin/ -COPY --from=dbclient-builder /postgresql-16.0/src/bin/pg_dump/pg_restore /usr/local/bin/ -COPY --from=dbclient-builder /tmp/mysql-community-client/usr/bin/mysql /usr/local/bin/ -COPY --from=dbclient-builder /tmp/mysql-community-client/usr/bin/mysqldump /usr/local/bin/ - COPY ./docker/nocobase/docker-entrypoint.sh /app/ CMD ["/app/docker-entrypoint.sh"] From 244bcddd1633ef7aca9284b9962c633ebc2dee5f Mon Sep 17 00:00:00 2001 From: gchust Date: Mon, 30 Sep 2024 21:19:43 +0800 Subject: [PATCH 68/68] fix: pg client has not been installed in pro image (#5348) --- Dockerfile.pro | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile.pro b/Dockerfile.pro index 4a01713c2f..10c571c0ad 100644 --- a/Dockerfile.pro +++ b/Dockerfile.pro @@ -4,7 +4,6 @@ ARG COMMIT_HASH ARG APPEND_PRESET_LOCAL_PLUGINS ARG BEFORE_PACK_NOCOBASE="ls -l" ARG PLUGINS_DIRS -ARG PG_CLIENT_VERSION=16 ENV PLUGINS_DIRS=${PLUGINS_DIRS} @@ -48,7 +47,7 @@ RUN rm -rf /etc/nginx/sites-enabled/default # install postgresql-client and mysql-client RUN apt update && apt install -y wget postgresql-common gnupg \ && /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y \ - && apt install -y postgresql-client-${PG_CLIENT_VERSION} \ + && apt install -y postgresql-client-16 \ && wget https://downloads.mysql.com/archives/get/p/23/file/mysql-community-client-core_8.1.0-1debian11_amd64.deb \ && dpkg -x mysql-community-client-core_8.1.0-1debian11_amd64.deb /tmp/mysql-client \ && cp /tmp/mysql-client/usr/bin/mysqldump /usr/bin/ \