diff --git a/.github/workflows/build-internal-image.yml b/.github/workflows/build-internal-image.yml new file mode 100644 index 0000000000..0601be438d --- /dev/null +++ b/.github/workflows/build-internal-image.yml @@ -0,0 +1,119 @@ +name: Build Image (Internal) + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + inputs: + ref_name: + description: 'Branch or tag name to release' + +jobs: + get-plugins: + uses: nocobase/nocobase/.github/workflows/get-plugins.yml@main + secrets: inherit + push-docker: + runs-on: ubuntu-latest + needs: get-plugins + services: + verdaccio: + image: verdaccio/verdaccio:5 + ports: + - 4873:4873 + steps: + - name: Set Node.js 20 + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Get info + id: get-info + shell: bash + run: | + if [[ "${{ inputs.ref_name || github.ref_name }}" =~ "beta" ]]; then + echo "defaultTag=$(echo 'beta')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.beta-plugins }}')" >> $GITHUB_OUTPUT + elif [[ "${{ inputs.ref_name || github.ref_name }}" =~ "alpha" ]]; then + echo "defaultTag=$(echo 'alpha')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.alpha-plugins }}')" >> $GITHUB_OUTPUT + else + # rc + echo "defaultTag=$(echo 'latest')" >> $GITHUB_OUTPUT + echo "proRepos=$(echo '${{ needs.get-plugins.outputs.rc-plugins }}')" >> $GITHUB_OUTPUT + fi + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ vars.NOCOBASE_APP_ID }} + private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }} + repositories: nocobase,pro-plugins,${{ join(fromJSON(steps.get-info.outputs.proRepos), ',') }},${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ',') }} + skip-token-revoke: true + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref_name || github.ref_name }} + - name: yarn install + run: | + yarn install + - name: Checkout pro-plugins + uses: actions/checkout@v3 + with: + repository: nocobase/pro-plugins + path: packages/pro-plugins + ref: ${{ inputs.ref_name || github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + - name: Clone pro repos + shell: bash + run: | + for repo in ${{ join(fromJSON(steps.get-info.outputs.proRepos), ' ') }} ${{ join(fromJSON(needs.get-plugins.outputs.custom-plugins), ' ') }} + do + git clone -b ${{ inputs.ref_name || github.ref_name }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo + done + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + 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: + registry: ${{ secrets.ALI_DOCKER_REGISTRY }} + username: ${{ secrets.ALI_DOCKER_USERNAME }} + password: ${{ secrets.ALI_DOCKER_PASSWORD }} + - name: Set variables + run: | + target_directory="./packages/pro-plugins/@nocobase" + subdirectories=$(find "$target_directory" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | tr '\n' ' ') + trimmed_variable=$(echo "$subdirectories" | xargs) + packageNames="@nocobase/${trimmed_variable// / @nocobase/}" + pluginNames="${trimmed_variable//plugin-/}" + BEFORE_PACK_NOCOBASE="yarn add @nocobase/plugin-notifications @nocobase/plugin-disable-pm-add $packageNames -W --production" + APPEND_PRESET_LOCAL_PLUGINS="notifications,disable-pm-add,${pluginNames// /,}" + echo "var1=$BEFORE_PACK_NOCOBASE" >> $GITHUB_OUTPUT + echo "var2=$APPEND_PRESET_LOCAL_PLUGINS" >> $GITHUB_OUTPUT + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + build-args: | + VERDACCIO_URL=http://localhost:4873/ + COMMIT_HASH=${GITHUB_SHA} + PLUGINS_DIRS=pro-plugins + BEFORE_PACK_NOCOBASE=${{ steps.vars.outputs.var1 }} + APPEND_PRESET_LOCAL_PLUGINS=${{ steps.vars.outputs.var2 }} + push: true + tags: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:${{ steps.get-info.outputs.defaultTag }},${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/nocobase-test-backend.yml b/.github/workflows/nocobase-test-backend.yml index f7956dddd7..ebeea8c41f 100644 --- a/.github/workflows/nocobase-test-backend.yml +++ b/.github/workflows/nocobase-test-backend.yml @@ -5,38 +5,40 @@ concurrency: cancel-in-progress: true on: - push: - branches: - - main - - next - - develop - paths: - - 'package.json' - - '**/yarn.lock' - - 'packages/core/acl/**' - - 'packages/core/auth/**' - - 'packages/core/actions/**' - - 'packages/core/database/**' - - 'packages/core/resourcer/**' - - 'packages/core/data-source-manager/**' - - 'packages/core/server/**' - - 'packages/core/utils/**' - - 'packages/plugins/**/src/server/**' - - '.github/workflows/nocobase-test-backend.yml' - pull_request: - paths: - - 'package.json' - - '**/yarn.lock' - - 'packages/core/acl/**' - - 'packages/core/auth/**' - - 'packages/core/actions/**' - - 'packages/core/database/**' - - 'packages/core/resourcer/**' - - 'packages/core/data-source-manager/**' - - 'packages/core/server/**' - - 'packages/core/utils/**' - - 'packages/plugins/**/src/server/**' - - '.github/workflows/nocobase-test-backend.yml' + workflow_dispatch: + + # push: + # branches: + # - main + # - next + # - develop + # paths: + # - 'package.json' + # - '**/yarn.lock' + # - 'packages/core/acl/**' + # - 'packages/core/auth/**' + # - 'packages/core/actions/**' + # - 'packages/core/database/**' + # - 'packages/core/resourcer/**' + # - 'packages/core/data-source-manager/**' + # - 'packages/core/server/**' + # - 'packages/core/utils/**' + # - 'packages/plugins/**/src/server/**' + # - '.github/workflows/nocobase-test-backend.yml' + # pull_request: + # paths: + # - 'package.json' + # - '**/yarn.lock' + # - 'packages/core/acl/**' + # - 'packages/core/auth/**' + # - 'packages/core/actions/**' + # - 'packages/core/database/**' + # - 'packages/core/resourcer/**' + # - 'packages/core/data-source-manager/**' + # - 'packages/core/server/**' + # - 'packages/core/utils/**' + # - 'packages/plugins/**/src/server/**' + # - '.github/workflows/nocobase-test-backend.yml' jobs: sqlite-test: diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a0eda99b..e50400bc4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,220 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17 + +### 🚀 Improvements + +- **[client]** Add delay API for scenarios which open without delay ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher + +- **[create-nocobase-app]** Upgrade some dependencies to latest versions ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos + +### 🐛 Bug Fixes + +- **[client]** + - Fix error thrown when mouse hover on referenced template block in approval node configuration ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher + + - custom association field not displaying field component settings ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh + + - Fix locale for upload component ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher + + - lazy load missing ui component will cause render error ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust + + - Add native Password component to HoC Input ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher + + - inherited fields shown in current collection field assignment list ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh + +- **[database]** Fixed ci build error ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie + +- **[build]** build output is incorrect when plugin depends on some AMD libraries ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust + +- **[Action: Import records]** fixed an error importing xlsx time field ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie + +- **[Workflow: Manual node]** Fix manual task status constant ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher + +- **[Block: iframe]** vertical scrollbar appears when iframe block is set to full height ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh + +- **[Workflow: Custom action event]** Fix test cases by @mytharcher + +- **[Backup manager]** timeout error occurs when trying to restore an unecrypted backup with a password by @gchust + +## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14 + +### 🎉 New Features + +- **[Departments]** Make Department, Attachment URL, and Workflow response message plugins free ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos + +### 🐛 Bug Fixes + +- **[client]** + - The filter form should not display the "Unsaved changes" prompt ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe + + - "allow multiple" option not working for relation field ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh + + - In the filter form, when the filter button is clicked, if there are fields that have not passed validation, the filtering is still triggered ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe + + - Switching to the group menu should not jump to a page that has already been hidden in menu ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe + +- **[File storage: S3(Pro)]** + - Organize language by @jiannx + + - Individual baseurl and public settings, improve S3 pro storage config UX by @jiannx + +- **[Migration manager]** the skip auto backup option becomes invalid if environment variable popup appears during migration by @gchust + +## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 + +### 🐛 Bug Fixes + +- **[client]** + - Fix the issue of preview images being obscured ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe + + - In the form block, the default value of the field configuration will first be displayed as the original variable string and then disappear ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe + +## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11 + +### 🚀 Improvements + +- **[client]** + - Add default type fallback API for `Variable.Input` ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher + + - Optimize prompts for unconfigured pages ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe + +- **[Workflow: Delay node]** Support to use variable for duration ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher + +- **[Workflow: Custom action event]** Add refresh settings for trigger workflow button by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** + - subtable description overlapping with add new button ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh + + - dashed underline caused by horizontal form layout in modal ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh + +- **[File storage: S3(Pro)]** Fix missing await for next call. by @jiannx + +- **[Email manager]** Fix missing await for next call. by @jiannx + +## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09 + +### 🚀 Improvements + +- **[utils]** Add duration extension for dayjs ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher + +- **[client]** + - Support to search field in Filter component ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher + + - Add `trim` API for `Input` and `Variable.TextArea` ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher + +- **[Error handler]** Support custom title in AppError component. ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66 + +- **[IP restriction]** Update IP restriction message content. by @sheldon66 + +- **[File storage: S3(Pro)]** Support global variables in storage configuration by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** + - rule with 'any' condition does not take effect when condition list is empty ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh + + - data issue with Gantt block in tree collection ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh + + - The relationship fields in the filter form report an error after the page is refreshed because x-data-source is not carried ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe + + - variable parse failure when URL parameters contain Chinese characters ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh + +- **[Users]** Issue with parsing the user profile form schema ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile + +- **[Mobile]** single-select field with 'contains' filter on mobile does not support multiple selection ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh + +- **[Action: Export records]** missing filter params when exporting data after changing pagination ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh + +- **[Email manager]** fix email management permission cannot view email list by @jiannx + +- **[File storage: S3(Pro)]** Throw error to user when upload logo to S3 Pro storage (set to default) by @mytharcher + +- **[Workflow: Approval]** Fix `updatedAt` changed after migration by @mytharcher + +- **[Migration manager]** migration log creation time is displayed incorrectly in some environments by @gchust + +## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03 + +### 🐛 Bug Fixes + +- **[client]** + - x-disabled property not taking effect on form fields ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh + + - field label display issue to prevent truncation by colon ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh + +- **[database]** When deleting one-to-many records, both `filter` and `filterByTk` are passed and `filter` includes an association field, the `filterByTk` is ignored ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile + +## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01 + +### 🚀 Improvements + +- **[database]** + - Add trim option for text field ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher + + - Add trim option for string field ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher + +- **[File manager]** Add trim option for text fields of storages collection ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher + +- **[Workflow]** Improve code ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher + +- **[Workflow: Approval]** Support to use block template for approval process form by @mytharcher + +### 🐛 Bug Fixes + +- **[database]** Avoid "datetimeNoTz" field changes when value not changed in updating record ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher + +- **[client]** + - association field (select) displaying N/A when exposing related collection fields ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh + + - Fix `disabled` property not works when `SchemaInitializerItem` has `items` ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher + + - cascade issue: 'The value of xxx cannot be in array format' when deleting and re-selecting ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh + +- **[Collection field: Many to many (array)]** Issue of filtering by fields in an association collection with a many to many (array) field ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile + +- **[Public forms]** View permissions include list and get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos + +- **[Authentication]** token assignment in `AuthProvider` ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile + +- **[Workflow]** Fix sync option display incorrectly ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher + +- **[Block: Map]** map management validation should not pass with space input ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh + +- **[Workflow: Approval]** + - Fix client variables to use in approval form by @mytharcher + + - Fix branch mode when `endOnReject` configured as `true` by @mytharcher + +## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29 + +### 🐛 Bug Fixes + +- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh + +- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile + +- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher + +## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28 + +### 🚀 Improvements + +- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos + +- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh + +- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust + +### 🐛 Bug Fixes + +- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh + +- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe + ## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27 ### 🐛 Bug Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index e22c5a3d4a..57ce420796 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,220 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +## [v1.6.21](https://github.com/nocobase/nocobase/compare/v1.6.20...v1.6.21) - 2025-04-17 + +### 🚀 优化 + +- **[client]** 为弹窗组件增加 delay API ([#6681](https://github.com/nocobase/nocobase/pull/6681)) by @mytharcher + +- **[create-nocobase-app]** 升级部分依赖的版本 ([#6673](https://github.com/nocobase/nocobase/pull/6673)) by @chenos + +### 🐛 修复 + +- **[client]** + - 修复审批节点配置中引用模板区块的添加按钮报错问题 ([#6691](https://github.com/nocobase/nocobase/pull/6691)) by @mytharcher + + - 自定义的关系字段没有显示关系字段组件 ([#6692](https://github.com/nocobase/nocobase/pull/6692)) by @katherinehhh + + - 修复上传组件语言问题 ([#6682](https://github.com/nocobase/nocobase/pull/6682)) by @mytharcher + + - 懒加载组件不存在时界面报错 ([#6683](https://github.com/nocobase/nocobase/pull/6683)) by @gchust + + - 补全原生的 Password 组件到封装过的输入组件 ([#6679](https://github.com/nocobase/nocobase/pull/6679)) by @mytharcher + + - 字段赋值本表字段列表中显示了继承表字段,应只显示本表字段 ([#6666](https://github.com/nocobase/nocobase/pull/6666)) by @katherinehhh + +- **[database]** 修复 CI 编译错误 ([#6687](https://github.com/nocobase/nocobase/pull/6687)) by @aaaaaajie + +- **[build]** 插件依赖 AMD 库时构建产物不正确 ([#6665](https://github.com/nocobase/nocobase/pull/6665)) by @gchust + +- **[操作:导入记录]** 修复导入包含时间字段的 xlsx 错误 ([#6672](https://github.com/nocobase/nocobase/pull/6672)) by @aaaaaajie + +- **[工作流:人工处理节点]** 修复人工节点任务状态常量 ([#6676](https://github.com/nocobase/nocobase/pull/6676)) by @mytharcher + +- **[区块:iframe]** iframe 区块设置全高时页面出现滚动条 ([#6675](https://github.com/nocobase/nocobase/pull/6675)) by @katherinehhh + +- **[工作流:自定义操作事件]** 修复测试用例 by @mytharcher + +- **[备份管理器]** 还原时若备份未设置密码,但用户输入了密码,还原会出现超时报错 by @gchust + +## [v1.6.20](https://github.com/nocobase/nocobase/compare/v1.6.19...v1.6.20) - 2025-04-14 + +### 🎉 新特性 + +- **[部门]** 商业插件部门、附件 URL、工作流响应消息改为免费提供 ([#6663](https://github.com/nocobase/nocobase/pull/6663)) by @chenos + +### 🐛 修复 + +- **[client]** + - 筛选表单不应该显示“未保存修改”提示 ([#6657](https://github.com/nocobase/nocobase/pull/6657)) by @zhangzhonghe + + - 筛选表单中关系字段的“允许多选”设置项不生效 ([#6661](https://github.com/nocobase/nocobase/pull/6661)) by @katherinehhh + + - 筛选表单中,当点击筛选按钮时,如果有字段未校验通过,依然会触发筛选的问题 ([#6659](https://github.com/nocobase/nocobase/pull/6659)) by @zhangzhonghe + + - 切换到分组菜单时,不应该跳转到已经在菜单中被隐藏的页面 ([#6654](https://github.com/nocobase/nocobase/pull/6654)) by @zhangzhonghe + +- **[文件存储:S3 (Pro)]** + - 整理语言文案 by @jiannx + + - baseurl 和 public 设置不再互相关联,改进 S3 pro 存储的配置交互体验 by @jiannx + +- **[迁移管理]** 迁移时若弹出环境变量弹窗,跳过自动备份选项会失效 by @gchust + +## [v1.6.19](https://github.com/nocobase/nocobase/compare/v1.6.18...v1.6.19) - 2025-04-14 + +### 🐛 修复 + +- **[client]** + - 修复预览图片被遮挡的问题 ([#6651](https://github.com/nocobase/nocobase/pull/6651)) by @zhangzhonghe + + - 表单区块中,字段配置的默认值会先显示为原始变量字符串然后再消失 ([#6649](https://github.com/nocobase/nocobase/pull/6649)) by @zhangzhonghe + +## [v1.6.18](https://github.com/nocobase/nocobase/compare/v1.6.17...v1.6.18) - 2025-04-11 + +### 🚀 优化 + +- **[client]** + - 为 `Variable.Input` 组件增加默认退避类型的 API ([#6644](https://github.com/nocobase/nocobase/pull/6644)) by @mytharcher + + - 优化未配置页面时的提示 ([#6641](https://github.com/nocobase/nocobase/pull/6641)) by @zhangzhonghe + +- **[工作流:延时节点]** 支持延迟时间使用变量 ([#6621](https://github.com/nocobase/nocobase/pull/6621)) by @mytharcher + +- **[工作流:自定义操作事件]** 为触发工作流按钮增加刷新配置项 by @mytharcher + +### 🐛 修复 + +- **[client]** + - 子表格中描述信息与操作按钮遮挡 ([#6646](https://github.com/nocobase/nocobase/pull/6646)) by @katherinehhh + + - 弹窗表单在 horizontal 布局下初始宽度计算错误,导致出现提示和 下划虚线 ([#6639](https://github.com/nocobase/nocobase/pull/6639)) by @katherinehhh + +- **[文件存储:S3 (Pro)]** 修复next调用缺少await by @jiannx + +- **[邮件管理]** 修复next调用缺少await by @jiannx + +## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09 + +### 🚀 优化 + +- **[utils]** 为 dayjs 包增加时长扩展 ([#6630](https://github.com/nocobase/nocobase/pull/6630)) by @mytharcher + +- **[client]** + - 支持筛选组件中对字段进行搜索 ([#6627](https://github.com/nocobase/nocobase/pull/6627)) by @mytharcher + + - 为 `Input` 和 `Variable.TextArea` 组件增加 `trim` API ([#6624](https://github.com/nocobase/nocobase/pull/6624)) by @mytharcher + +- **[错误处理器]** 在 AppError 组件中支持自定义标题。 ([#6409](https://github.com/nocobase/nocobase/pull/6409)) by @sheldon66 + +- **[IP 限制]** 更新 IP 限制消息内容。 by @sheldon66 + +- **[文件存储:S3 (Pro)]** 支持存储引擎的配置中使用全局变量 by @mytharcher + +### 🐛 修复 + +- **[client]** + - 联动规则条件设置为任意且无条件内容时属性设置不生效 ([#6628](https://github.com/nocobase/nocobase/pull/6628)) by @katherinehhh + + - 树表使用甘特图区块时数据显示异常 ([#6617](https://github.com/nocobase/nocobase/pull/6617)) by @katherinehhh + + - 筛选表单中的关系字段在刷新页面后,由于没有携带 x-data-source 而报错 ([#6619](https://github.com/nocobase/nocobase/pull/6619)) by @zhangzhonghe + + - 链接中中文参数变量值解析失败 ([#6618](https://github.com/nocobase/nocobase/pull/6618)) by @katherinehhh + +- **[用户]** 用户个人资料表单 schema 的解析问题 ([#6635](https://github.com/nocobase/nocobase/pull/6635)) by @2013xile + +- **[移动端]** 下拉单选字段在移动端设置筛选符为包含时组件未支持多选 ([#6629](https://github.com/nocobase/nocobase/pull/6629)) by @katherinehhh + +- **[操作:导出记录]** 筛选数据后切换分页再导出时筛选参数丢失 ([#6633](https://github.com/nocobase/nocobase/pull/6633)) by @katherinehhh + +- **[邮件管理]** 邮件管理权限无法查看邮件列表 by @jiannx + +- **[文件存储:S3 (Pro)]** 当用户上传 logo 失败时提示错误(设置为默认存储的 S3 Pro) by @mytharcher + +- **[工作流:审批]** 修复更新时间在迁移后变化 by @mytharcher + +- **[迁移管理]** 部分服务器环境下迁移日志创建日期显示不正确 by @gchust + +## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03 + +### 🐛 修复 + +- **[client]** + - 表单字段设置不可编辑不起作用 ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh + + - 表单字段标题因冒号导致的截断问题 ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh + +- **[database]** 删除一对多记录时,同时传递 `filter` 和 `filterByTk` 参数,`filter` 包含关系字段时,`filterByTk` 参数失效 ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile + +## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01 + +### 🚀 优化 + +- **[database]** + - 为多行文本类型字段增加去除首尾空白字符的选项 ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher + + - 为单行文本增加自动去除首尾空白字符的选项 ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher + +- **[文件管理器]** 为存储引擎表的文本字段增加去除首尾空白字符的选项 ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher + +- **[工作流]** 优化代码 ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher + +- **[工作流:审批]** 支持审批表单使用区块模板 by @mytharcher + +### 🐛 修复 + +- **[database]** 避免“日期时间(无时区)”字段在值未变动的更新时触发值改变 ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher + +- **[client]** + - 关系字段(select)放出关系表字段时默认显示 N/A ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh + + - 修复 `SchemaInitializerItem` 配置了 `items` 时 `disabled` 属性无效的问题 ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher + + - 级联组件删除后重新选择时出现 'The value of xxx cannot be in array format' ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh + +- **[数据表字段:多对多 (数组)]** 主表筛选带有多对多(数组)字段的关联表中的字段报错的问题 ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile + +- **[公开表单]** 查看权限包括 list 和 get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos + +- **[用户认证]** `AuthProvider` 中的 token 赋值 ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile + +- **[工作流]** 修复同步选项展示问题 ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher + +- **[区块:地图]** 地图管理必填校验不应通过空格输入 ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh + +- **[工作流:审批]** + - 修复审批表单中的前端变量 by @mytharcher + + - 修复分支模式下配置拒绝则结束时的流程问题 by @mytharcher + +## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29 + +### 🐛 修复 + +- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh + +- **[认证:OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile + +- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher + +## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28 + +### 🚀 优化 + +- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos + +- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh + +- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust + +### 🐛 修复 + +- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh + +- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe + ## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27 ### 🐛 修复 diff --git a/LICENSE.txt b/LICENSE.txt index b3c87c1e83..babf2bf053 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Updated Date: February 20, 2025 +Updated Date: April 1, 2025 NocoBase License Agreement @@ -88,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr 6.6 Can sell plugins developed for Software in the Marketplace. -6.7 The User with an Enterprise Edition License can sell Upper Layer Application to their clients. +6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients. 6.8 Not restricted by the AGPL-3.0 agreement. @@ -106,9 +106,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr 7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. -7.5 It is not allowed for the User withot an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license. +7.5 It is not allowed for the User withot a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license. -7.6 It is not allowed for the User with an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration. +7.6 It is not allowed for the User with a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration. 7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. diff --git a/README.ja-JP.md b/README.ja-JP.md index 2fff33bddd..49e5c1be42 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -2,14 +2,10 @@ https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17 -## ご協力ありがとうございます! +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## リリースノート - -リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。 +

## NocoBaseはなに? @@ -28,6 +24,16 @@ https://docs-cn.nocobase.com/ コミュニティ: https://forum.nocobase.com/ +チュートリアル: +https://www.nocobase.com/ja/tutorials + +顧客のストーリー: +https://www.nocobase.com/ja/blog/tags/customer-stories + +## リリースノート + +リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。 + ## 他の製品との違い ### 1. データモデル駆動 diff --git a/README.md b/README.md index 1314bec051..a2cfdb2bc4 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0 -## We'd love your support! - +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## Release Notes - -Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary. +

## What is NocoBase -NocoBase is a scalability-first, open-source no-code development platform. +NocoBase is an extensibility-first, open-source no-code development platform. Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform! Homepage: @@ -29,6 +24,17 @@ https://docs.nocobase.com/ Forum: https://forum.nocobase.com/ +Tutorials: +https://www.nocobase.com/en/tutorials + +Use Cases: +https://www.nocobase.com/en/blog/tags/customer-stories + + +## Release Notes + +Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary. + ## Distinctive features ### 1. Data model-driven diff --git a/README.zh-CN.md b/README.zh-CN.md index 66e9c281d7..728695efc7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,13 +2,10 @@ https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553 -## 感谢支持 +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## 发布日志 -我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。 +

## NocoBase 是什么 @@ -27,6 +24,15 @@ https://docs-cn.nocobase.com/ 社区: https://forum.nocobase.com/ +教程: +https://www.nocobase.com/cn/tutorials + +用户故事: +https://www.nocobase.com/cn/blog/tags/customer-stories + +## 发布日志 +我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。 + ## 与众不同之处 ### 1. 数据模型驱动 diff --git a/docker/nocobase/Dockerfile b/docker/nocobase/Dockerfile index 5de0d6030a..3fb6bafc89 100644 --- a/docker/nocobase/Dockerfile +++ b/docker/nocobase/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app RUN cd /app \ && yarn config set network-timeout 600000 -g \ - && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app -a -e APP_ENV=production \ + && npx -y create-nocobase-app@${CNA_VERSION} my-nocobase-app --skip-dev-dependencies -a -e APP_ENV=production \ && cd /app/my-nocobase-app \ && yarn install --production diff --git a/lerna.json b/lerna.json index 994c7d1426..8b921fd5ee 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "npmClient": "yarn", "useWorkspaces": true, "npmClientArgs": ["--ignore-engines"], diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json index cddc749497..0702ee69e7 100644 --- a/packages/core/acl/package.json +++ b/packages/core/acl/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/acl", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/resourcer": "1.7.0-alpha.10", - "@nocobase/utils": "1.7.0-alpha.10", + "@nocobase/resourcer": "1.7.0-alpha.11", + "@nocobase/utils": "1.7.0-alpha.11", "minimatch": "^5.1.1" }, "repository": { diff --git a/packages/core/acl/src/utils/acl-role.ts b/packages/core/acl/src/utils/acl-role.ts index 406ce36dec..8d3be6ba97 100644 --- a/packages/core/acl/src/utils/acl-role.ts +++ b/packages/core/acl/src/utils/acl-role.ts @@ -30,9 +30,65 @@ export function mergeRole(roles: ACLRole[]) { } } result.snippets = mergeRoleSnippets(allSnippets); + adjustActionByStrategy(roles, result); return result; } +/** + * When merging permissions from multiple roles, if strategy.actions allows certain actions, then those actions have higher priority. + * For example, [ + * { + * actions: { + * 'users:view': {...}, + * 'users:create': {...} + * }, + * strategy: { + * actions: ['view'] + * } + * }] + * finally result: [{ + * actions: { + * 'users:create': {...}, + * 'users:view': {} // all view + * }, + * { + * strategy: { + * actions: ['view'] + * }] + **/ +function adjustActionByStrategy( + roles, + result: { + actions?: Record; + strategy?: { actions?: string[] }; + resources?: string[]; + }, +) { + const { actions, strategy } = result; + const actionSet = getAdjustActions(roles); + if (!_.isEmpty(actions) && !_.isEmpty(strategy?.actions) && !_.isEmpty(result.resources)) { + for (const resource of result.resources) { + for (const action of strategy.actions) { + if (actionSet.has(action)) { + actions[`${resource}:${action}`] = {}; + } + } + } + } +} + +function getAdjustActions(roles: ACLRole[]) { + const actionSet = new Set(); + for (const role of roles) { + const jsonRole = role.toJSON(); + // Within the same role, actions have higher priority than strategy.actions. + if (!_.isEmpty(jsonRole.strategy?.['actions']) && _.isEmpty(jsonRole.actions)) { + jsonRole.strategy['actions'].forEach((x) => !x.includes('own') && actionSet.add(x)); + } + } + return actionSet; +} + function mergeRoleNames(sourceRoleNames, newRoleName) { return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames; } diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index 441dc3e596..95d4f210e6 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -1,14 +1,14 @@ { "name": "@nocobase/actions", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/cache": "1.7.0-alpha.10", - "@nocobase/database": "1.7.0-alpha.10", - "@nocobase/resourcer": "1.7.0-alpha.10" + "@nocobase/cache": "1.7.0-alpha.11", + "@nocobase/database": "1.7.0-alpha.11", + "@nocobase/resourcer": "1.7.0-alpha.11" }, "repository": { "type": "git", diff --git a/packages/core/app/package.json b/packages/core/app/package.json index 34ab32b74f..b125d3f42b 100644 --- a/packages/core/app/package.json +++ b/packages/core/app/package.json @@ -1,17 +1,17 @@ { "name": "@nocobase/app", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/database": "1.7.0-alpha.10", - "@nocobase/preset-nocobase": "1.7.0-alpha.10", - "@nocobase/server": "1.7.0-alpha.10" + "@nocobase/database": "1.7.0-alpha.11", + "@nocobase/preset-nocobase": "1.7.0-alpha.11", + "@nocobase/server": "1.7.0-alpha.11" }, "devDependencies": { - "@nocobase/client": "1.7.0-alpha.10" + "@nocobase/client": "1.7.0-alpha.11" }, "repository": { "type": "git", diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index 8730279583..70eb7e8de9 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -1,16 +1,16 @@ { "name": "@nocobase/auth", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.7.0-alpha.10", - "@nocobase/cache": "1.7.0-alpha.10", - "@nocobase/database": "1.7.0-alpha.10", - "@nocobase/resourcer": "1.7.0-alpha.10", - "@nocobase/utils": "1.7.0-alpha.10", + "@nocobase/actions": "1.7.0-alpha.11", + "@nocobase/cache": "1.7.0-alpha.11", + "@nocobase/database": "1.7.0-alpha.11", + "@nocobase/resourcer": "1.7.0-alpha.11", + "@nocobase/utils": "1.7.0-alpha.11", "@types/jsonwebtoken": "^8.5.8", "jsonwebtoken": "^8.5.1" }, diff --git a/packages/core/build/package.json b/packages/core/build/package.json index 3a5a95f97d..447fdf10e7 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/build", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "Library build tool based on rollup.", "main": "lib/index.js", "types": "./lib/index.d.ts", @@ -17,7 +17,7 @@ "@lerna/project": "4.0.0", "@rsbuild/plugin-babel": "^1.0.3", "@rsdoctor/rspack-plugin": "^0.4.8", - "@rspack/core": "1.1.1", + "@rspack/core": "1.3.2", "@svgr/webpack": "^8.1.0", "@types/gulp": "^4.0.13", "@types/lerna__package": "5.1.0", @@ -39,7 +39,7 @@ "postcss-preset-env": "^9.1.2", "react-imported-component": "^6.5.4", "style-loader": "^3.3.3", - "tar": "^6.2.0", + "tar": "^7.4.3", "tsup": "8.2.4", "typescript": "5.1.3", "update-notifier": "3.0.0", diff --git a/packages/core/build/src/buildPlugin.ts b/packages/core/build/src/buildPlugin.ts index f851f37ed8..9b7f228072 100644 --- a/packages/core/build/src/buildPlugin.ts +++ b/packages/core/build/src/buildPlugin.ts @@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou umdNamedDefine: true, }, }, + amd: {}, resolve: { tsConfig: path.join(process.cwd(), 'tsconfig.json'), extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'], diff --git a/packages/core/build/src/tarPlugin.ts b/packages/core/build/src/tarPlugin.ts index 1d58224924..35b107e5aa 100644 --- a/packages/core/build/src/tarPlugin.ts +++ b/packages/core/build/src/tarPlugin.ts @@ -8,7 +8,7 @@ */ import path from 'path'; -import tar from 'tar'; +import { create } from 'tar'; import fg from 'fast-glob'; import fs from 'fs-extra'; @@ -38,5 +38,5 @@ export function tarPlugin(cwd: string, log: PkgLog) { fs.mkdirpSync(path.dirname(tarball)); fs.rmSync(tarball, { force: true }); - return tar.c({ gzip: true, file: tarball, cwd }, tarFiles); + return create({ gzip: true, file: tarball, cwd }, tarFiles); } diff --git a/packages/core/cache/package.json b/packages/core/cache/package.json index e9d9b21e92..1721f06553 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -1,12 +1,12 @@ { "name": "@nocobase/cache", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/lock-manager": "1.7.0-alpha.10", + "@nocobase/lock-manager": "1.7.0-alpha.11", "bloom-filters": "^3.0.1", "cache-manager": "^5.2.4", "cache-manager-redis-yet": "^4.1.2" diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index f245798780..1d9d20fa68 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cli", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", @@ -8,7 +8,7 @@ "nocobase": "./bin/index.js" }, "dependencies": { - "@nocobase/app": "1.7.0-alpha.10", + "@nocobase/app": "1.7.0-alpha.11", "@types/fs-extra": "^11.0.1", "@umijs/utils": "3.5.20", "chalk": "^4.1.1", @@ -18,14 +18,14 @@ "fast-glob": "^3.3.1", "fs-extra": "^11.1.1", "p-all": "3.0.0", - "pm2": "^5.2.0", + "pm2": "^6.0.5", "portfinder": "^1.0.28", - "serve": "^13.0.2", + "tar": "^7.4.3", "tree-kill": "^1.2.2", "tsx": "^4.19.0" }, "devDependencies": { - "@nocobase/devtools": "1.7.0-alpha.10" + "@nocobase/devtools": "1.7.0-alpha.11" }, "repository": { "type": "git", diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 742c60cb41..1c760cd4ba 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -460,8 +460,16 @@ exports.initEnv = function initEnv() { process.env.SOCKET_PATH = generateGatewayPath(); fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true }); fs.mkdirpSync(process.env.PM2_HOME, { force: true, recursive: true }); - const pkgDir = resolve(process.cwd(), 'storage/plugins', '@nocobase/plugin-multi-app-manager'); - fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { force: true }); + const pkgs = [ + '@nocobase/plugin-multi-app-manager', + '@nocobase/plugin-departments', + '@nocobase/plugin-field-attachment-url', + '@nocobase/plugin-workflow-response-message', + ]; + for (const pkg of pkgs) { + const pkgDir = resolve(process.cwd(), 'storage/plugins', pkg); + fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { recursive: true, force: true }); + } }; exports.generatePlugins = function () { diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index 1debccd479..8389585d18 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -234,6 +234,10 @@ export default defineConfig({ "title": "Filter", "link": "/components/filter" }, + { + "title": "LinkageFilter", + "link": "/components/linkage-filter" + }, ] }, { diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 327079732f..dabb49c8d3 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/client", - "version": "1.7.0-alpha.10", + "version": "1.7.0-alpha.11", "license": "AGPL-3.0", "main": "lib/index.js", "module": "es/index.mjs", @@ -27,10 +27,10 @@ "@formily/reactive-react": "^2.2.27", "@formily/shared": "^2.2.27", "@formily/validator": "^2.2.27", - "@nocobase/evaluators": "1.7.0-alpha.10", - "@nocobase/sdk": "1.7.0-alpha.10", - "@nocobase/utils": "1.7.0-alpha.10", - "@nocobase/json-template-parser": "1.7.0-alpha.10", + "@nocobase/evaluators": "1.7.0-alpha.11", + "@nocobase/sdk": "1.7.0-alpha.11", + "@nocobase/utils": "1.7.0-alpha.11", + "@nocobase/json-template-parser": "1.7.0-alpha.11", "ahooks": "^3.7.2", "antd": "5.24.2", "antd-style": "3.7.1", diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 19d8433475..478bacb68f 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -314,15 +314,15 @@ export const ACLActionProvider = (props) => { const schema = useFieldSchema(); const currentUid = schema['x-uid']; let actionPath = schema['x-acl-action']; - const editablePath = ['create', 'update', 'destroy', 'importXlsx']; + // 只兼容这些数据表资源按钮 + const resourceActionPath = ['create', 'update', 'destroy', 'importXlsx', 'export']; - if (!actionPath && resource && schema['x-action'] && editablePath.includes(schema['x-action'])) { + if (!actionPath && resource && schema['x-action'] && resourceActionPath.includes(schema['x-action'])) { actionPath = `${resource}:${schema['x-action']}`; } if (actionPath && !actionPath?.includes(':')) { actionPath = `${resource}:${actionPath}`; } - const params = useMemo( () => actionPath && parseAction(actionPath, { schema, recordPkValue }), [parseAction, actionPath, schema, recordPkValue], @@ -340,7 +340,7 @@ export const ACLActionProvider = (props) => { return {props.children}; } //视图表无编辑权限时不显示 - if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) { + if (resourceActionPath.includes(actionPath) || resourceActionPath.includes(actionPath?.split(':')[1])) { if ((collection && collection.template !== 'view') || collection?.writableView) { return {props.children}; } diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 3fe2711a2a..f85c766e10 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -150,6 +150,9 @@ export class APIClient extends APIClientSDK { } return [{ message }]; } + if (error?.response?.data?.error) { + return [error?.response?.data?.error]; + } return ( error?.response?.data?.errors || error?.response?.data?.messages || diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx index aeb2c475b9..31801e97ed 100644 --- a/packages/core/client/src/application/components/defaultComponents.tsx +++ b/packages/core/client/src/application/components/defaultComponents.tsx @@ -11,10 +11,11 @@ import React, { FC } from 'react'; import { MainComponent } from './MainComponent'; const Loading: FC = () =>
Loading...
; -const AppError: FC<{ error: Error }> = ({ error }) => { +const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => { + const title = error?.title || 'App Error'; return (
-
App Error
+
{title}
{error?.message} {process.env.__TEST__ && error?.stack}
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx index 436fc54f37..dfdedad636 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx @@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo( className: className, label: children || compile(title), onClick: (info) => { - if (info.key !== name) return; + if (disabled || info.key !== name) return; if (closeInitializerMenuWhenClick) { setVisible?.(false); } @@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo( children: childrenItems, }, ]; - }, [name, style, className, children, title, onClick, icon, childrenItems]); + }, [name, disabled, style, className, children, title, onClick, icon, childrenItems]); if (items && items.length > 0) { - return ; + return ; } return (
{ const filedSchema = useFieldSchema(); @@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { }} > false}> - + diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 9dfead7b02..d34a8d540e 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -546,9 +546,11 @@ export const useFilterBlockActionProps = () => { const { doFilter } = useDoFilter(); const actionField = useField(); actionField.data = actionField.data || {}; + const form = useForm(); return { async onClick() { + await form.submit(); actionField.data.loading = true; await doFilter(); actionField.data.loading = false; @@ -1580,7 +1582,7 @@ export const getAppends = ({ const fieldNames = getTargetField(item); // 只应该收集关系字段,只有大于 1 的时候才是关系字段 - if (fieldNames.length > 1) { + if (fieldNames.length > 1 && !item.op) { appends.add(fieldNames.join('.')); } }); diff --git a/packages/core/client/src/collection-manager/interfaces/input.ts b/packages/core/client/src/collection-manager/interfaces/input.ts index ac5897adb7..40d131e5be 100644 --- a/packages/core/client/src/collection-manager/interfaces/input.ts +++ b/packages/core/client/src/collection-manager/interfaces/input.ts @@ -62,6 +62,12 @@ export class InputFieldInterface extends CollectionFieldInterface { hasDefaultValue = true; properties = { ...defaultProps, + trim: { + type: 'boolean', + 'x-content': '{{t("Automatically remove heading and tailing spaces")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, layout: { type: 'void', title: '{{t("Index")}}', 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 ce0b6d411f..80b4e31527 100644 --- a/packages/core/client/src/collection-manager/interfaces/properties/operators.ts +++ b/packages/core/client/src/collection-manager/interfaces/properties/operators.ts @@ -129,12 +129,12 @@ export const enumType = [ label: '{{t("is")}}', value: '$eq', selected: true, - schema: { 'x-component': 'Select' }, + schema: { 'x-component': 'Select', 'x-component-props': { mode: null } }, }, { label: '{{t("is not")}}', value: '$ne', - schema: { 'x-component': 'Select' }, + schema: { 'x-component': 'Select', 'x-component-props': { mode: null } }, }, { label: '{{t("is any of")}}', diff --git a/packages/core/client/src/collection-manager/interfaces/textarea.ts b/packages/core/client/src/collection-manager/interfaces/textarea.ts index 2ccebbdeb6..19f48bc4c1 100644 --- a/packages/core/client/src/collection-manager/interfaces/textarea.ts +++ b/packages/core/client/src/collection-manager/interfaces/textarea.ts @@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface { titleUsable = true; properties = { ...defaultProps, + trim: { + type: 'boolean', + 'x-content': '{{t("Automatically remove heading and tailing spaces")}}', + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, }; schemaInitialize(schema: ISchema, { block }) { if (['Table', 'Kanban'].includes(block)) { diff --git a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts index 5399efe4c1..5648500ddc 100644 --- a/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts +++ b/packages/core/client/src/collection-manager/mixins/InheritanceCollectionMixin.ts @@ -7,9 +7,9 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { filter, unionBy, uniq } from 'lodash'; import type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source'; import { Collection } from '../../data-source/collection/Collection'; -import _, { filter, unionBy, uniq } from 'lodash'; export class InheritanceCollectionMixin extends Collection { protected parentCollectionsName: string[]; @@ -22,6 +22,7 @@ export class InheritanceCollectionMixin extends Collection { protected parentCollectionFields: Record = {}; protected allCollectionsInheritChain: string[]; protected inheritCollectionsChain: string[]; + protected inheritChain: string[]; protected foreignKeyFields: CollectionFieldOptions[]; getParentCollectionsName() { @@ -233,6 +234,43 @@ export class InheritanceCollectionMixin extends Collection { return this.inheritCollectionsChain; } + /** + * 获取所有祖先数据表和后代数据表,不包括兄弟表。用于下面这些地方: + * - 筛选区块链接数据区块时使用 + */ + getInheritChain() { + if (this.inheritChain) { + return this.inheritChain.slice(); + } + + const ancestorChain = this.getInheritCollectionsChain(); + const descendantNames = this.getChildrenCollectionsName(); + + // 构建最终的链,首先包含祖先链(包括自身) + const inheritChain = [...ancestorChain]; + + // 再添加直接后代及其后代,但不包括兄弟表 + const addDescendants = (names: string[]) => { + for (const name of names) { + if (!inheritChain.includes(name)) { + inheritChain.push(name); + const childCollection = this.collectionManager.getCollection(name); + if (childCollection) { + // 递归添加每个后代的后代 + const childrenNames = childCollection.getChildrenCollectionsName(); + addDescendants(childrenNames); + } + } + } + }; + + // 从当前集合的直接后代开始添加 + addDescendants(descendantNames); + + this.inheritChain = inheritChain; + return this.inheritChain; + } + getAllFields(predicate?: GetCollectionFieldPredicate) { if (this.allFields) { return this.allFields.slice(); diff --git a/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts new file mode 100644 index 0000000000..b3bd5c4613 --- /dev/null +++ b/packages/core/client/src/collection-manager/mixins/__tests__/InheritanceCollectionMixin.test.ts @@ -0,0 +1,189 @@ +import { Application } from '@nocobase/client'; +import { CollectionManager } from '../../../data-source/collection/CollectionManager'; +import { InheritanceCollectionMixin } from '../InheritanceCollectionMixin'; + +describe('InheritanceCollectionMixin', () => { + let app: Application; + let collectionManager: CollectionManager; + + beforeEach(() => { + app = new Application({ + dataSourceManager: { + collectionMixins: [InheritanceCollectionMixin], + }, + }); + collectionManager = app.getCollectionManager(); + }); + + describe('getInheritChain', () => { + it('should return itself when there are no ancestors or descendants', () => { + const options = { + name: 'test', + fields: [{ name: 'field1', interface: 'input' }], + }; + + collectionManager.addCollections([options]); + const collection = collectionManager.getCollection('test'); + + const inheritChain = collection.getInheritChain(); + expect(inheritChain).toEqual(['test']); + }); + + it('should return a chain including all ancestor tables', () => { + // 创建三代数据表结构:grandparent -> parent -> child + const grandparentOptions = { + name: 'grandparent', + fields: [{ name: 'field1', interface: 'input' }], + }; + const parentOptions = { + name: 'parent', + inherits: ['grandparent'], + fields: [{ name: 'field2', interface: 'input' }], + }; + const childOptions = { + name: 'child', + inherits: ['parent'], + fields: [{ name: 'field3', interface: 'input' }], + }; + + // 先将所有集合添加到 collectionManager + collectionManager.addCollections([grandparentOptions, parentOptions, childOptions]); + + // 获取最终的集合实例以调用方法 + const child = collectionManager.getCollection('child'); + + // 测试 child 的继承链包含所有祖先表 + const inheritChain = child.getInheritChain(); + expect(inheritChain).toContain('child'); + expect(inheritChain).toContain('parent'); + expect(inheritChain).toContain('grandparent'); + expect(inheritChain.length).toBe(3); + }); + + it('should include all descendant tables, but not sibling tables', () => { + // 创建具有兄弟和后代关系的数据表结构 + // parent (祖先表) + // |-- child1 (子表) + // | |-- grandChild1 (孙表1) + // | |-- grandChild2 (孙表2) + // |-- child2 (兄弟表) + // |-- grandChild3 (兄弟的子表,不应该包括在测试集合的继承链中) + + const collections = [ + { + name: 'parent', + fields: [{ name: 'parentField', interface: 'input' }], + }, + { + name: 'child1', + inherits: ['parent'], + fields: [{ name: 'child1Field', interface: 'input' }], + }, + { + name: 'child2', + inherits: ['parent'], + fields: [{ name: 'child2Field', interface: 'input' }], + }, + { + name: 'grandChild1', + inherits: ['child1'], + fields: [{ name: 'grandChild1Field', interface: 'input' }], + }, + { + name: 'grandChild2', + inherits: ['child1'], + fields: [{ name: 'grandChild2Field', interface: 'input' }], + }, + { + name: 'grandChild3', + inherits: ['child2'], + fields: [{ name: 'grandChild3Field', interface: 'input' }], + }, + ]; + + // 一次性添加所有集合 + collectionManager.addCollections(collections); + + // 获取要测试的集合实例 + const child1 = collectionManager.getCollection('child1'); + + // 测试 child1 的继承链 + const child1InheritChain = child1.getInheritChain(); + + // 应该包含自身、父表和子表 + expect(child1InheritChain).toContain('child1'); + expect(child1InheritChain).toContain('parent'); + expect(child1InheritChain).toContain('grandChild1'); + expect(child1InheritChain).toContain('grandChild2'); + + // 不应该包含兄弟表及其子表 + expect(child1InheritChain).not.toContain('child2'); + expect(child1InheritChain).not.toContain('grandChild3'); + + // 检查总数量是否正确 (parent, child1, grandChild1, grandChild2) + expect(child1InheritChain.length).toBe(4); + }); + + it('should properly handle multiple inheritance', () => { + // 创建多重继承的数据表结构 + // parent1 parent2 + // \ / + // \ / + // child + // | + // grandChild + + const collections = [ + { + name: 'parent1', + fields: [{ name: 'parent1Field', interface: 'input' }], + }, + { + name: 'parent2', + fields: [{ name: 'parent2Field', interface: 'input' }], + }, + { + name: 'child', + inherits: ['parent1', 'parent2'], + fields: [{ name: 'childField', interface: 'input' }], + }, + { + name: 'grandChild', + inherits: ['child'], + fields: [{ name: 'grandChildField', interface: 'input' }], + }, + ]; + + // 一次性添加所有集合 + collectionManager.addCollections(collections); + + // 获取要测试的集合实例 + const child = collectionManager.getCollection('child'); + const grandChild = collectionManager.getCollection('grandChild'); + + // 测试 child 的继承链 + const childInheritChain = child.getInheritChain(); + + // 应该包含自身、两个父表和子表 + expect(childInheritChain).toContain('child'); + expect(childInheritChain).toContain('parent1'); + expect(childInheritChain).toContain('parent2'); + expect(childInheritChain).toContain('grandChild'); + + // 检查总数量是否正确 (child, parent1, parent2, grandChild) + expect(childInheritChain.length).toBe(4); + + // 测试 grandChild 的继承链 + const grandChildInheritChain = grandChild.getInheritChain(); + + // 应该包含自身及所有祖先表 + expect(grandChildInheritChain).toContain('grandChild'); + expect(grandChildInheritChain).toContain('child'); + expect(grandChildInheritChain).toContain('parent1'); + expect(grandChildInheritChain).toContain('parent2'); + + // 检查总数量是否正确 (grandChild, child, parent1, parent2) + expect(grandChildInheritChain.length).toBe(4); + }); + }); +}); diff --git a/packages/core/client/src/common/SelectWithTitle.tsx b/packages/core/client/src/common/SelectWithTitle.tsx index 4bd43648a3..b3e0be59a8 100644 --- a/packages/core/client/src/common/SelectWithTitle.tsx +++ b/packages/core/client/src/common/SelectWithTitle.tsx @@ -18,7 +18,14 @@ export interface SelectWithTitleProps { onChange?: (...args: any[]) => void; } -export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) { +export function SelectWithTitle({ + title, + defaultValue, + onChange, + options, + fieldNames, + ...others +}: SelectWithTitleProps) { const [open, setOpen] = useState(false); const timerRef = useRef(null); return ( @@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN > {title} + ); +} + export function AfterSuccess() { const { dn } = useDesignable(); const { t } = useTranslation(); const fieldSchema = useFieldSchema(); const { onSuccess } = fieldSchema?.['x-action-settings'] || {}; const environmentVariables = useGlobalVariable('$env'); + const templatePlugin: any = usePlugin('@nocobase/plugin-block-template'); + const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.(); + return ( useVariableProps(environmentVariables), }, + blocksToRefresh: { + type: 'array', + title: t('Refresh data blocks'), + 'x-decorator': 'FormItem', + 'x-use-decorator-props': () => { + return { + tooltip: t('After successful submission, the selected data blocks will be automatically refreshed.'), + }; + }, + 'x-component': BlocksSelector, + 'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置 + }, }, } as ISchema } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index 49d5eaaf60..fa8e7442a9 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -10,6 +10,7 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { Drawer } from 'antd'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; @@ -22,6 +23,7 @@ import { useActionContext } from './hooks'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; const MemoizeRecursionField = React.memo(RecursionField); MemoizeRecursionField.displayName = 'MemoizeRecursionField'; @@ -81,6 +83,7 @@ export const InternalActionDrawer: React.FC = observer( const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext(); const schema = useFieldSchema(); const field = useField(); + const { t } = useTranslation(); const { componentCls, hashId } = useStyles(); const tabContext = useTabsContext(); const parentZIndex = useZIndexContext(); @@ -118,7 +121,6 @@ export const InternalActionDrawer: React.FC = observer( }, [footerNodeName], ); - return ( @@ -126,7 +128,7 @@ export const InternalActionDrawer: React.FC = observer( { + if (ready) { + return; + } + if (visible) { + const timer = setTimeout(() => setReady(true), delay); + return () => clearTimeout(timer); + } else { + setReady(false); + } + }, [delay, ready, visible]); + return ready; +} + export const InternalActionModal: React.FC> = observer( (props) => { - const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, ...others } = props; + const { footerNodeName = 'Action.Modal.Footer', width, zIndex: _zIndex, delay = 200, ...others } = props; const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext(); const actualWidth = width ?? openSizeWidthMap.get(openSize); const schema = useFieldSchema(); @@ -90,6 +105,7 @@ export const InternalActionModal: React.FC> = obse } const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0); + const ready = useDelayedVisible(visible, delay); // 200ms 与 Modal 动画时间一致 return ( @@ -154,7 +170,11 @@ export const InternalActionModal: React.FC> = obse ) } > - + {ready ? ( + + ) : ( + + )} 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 22a168b62b..bacd353998 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -48,9 +48,12 @@ import { ActionContextProvider } from './context'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { linkageAction, setInitialActionState } from './utils'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; +import { BlockContext } from '../../../block-provider/BlockProvider'; // 这个要放到最下面,否则会导致前端单测失败 import { useApp } from '../../../application'; +import { useAllDataBlocks } from '../page/AllDataBlocksProvider'; const useA = () => { return { @@ -94,12 +97,16 @@ export const Action: ComposedAction = withDynamicSchemaProps( const { designable } = useDesignable(); const tarComponent = useComponent(component) || component; const variables = useVariables(); - const localVariables = useLocalVariables({ currentForm: { values: recordData, readPretty: false } as any }); + const localVariables = useLocalVariables({ + currentForm: { values: recordData, readPretty: false } as any, + }); const { visibleWithURL, setVisibleWithURL } = usePopupUtils(); const { setSubmitted } = useActionContext(); const { getAriaLabel } = useGetAriaLabelOfAction(title); const parentRecordData = useCollectionParentRecordData(); const app = useApp(); + const { getAllDataBlocks } = useAllDataBlocks(); + useEffect(() => { if (field.stateOfLinkageRules) { setInitialActionState(field); @@ -116,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( condition: v.condition, variables, localVariables, + conditionType: v.conditionType, }, app.jsonLogic, ); @@ -130,37 +138,62 @@ export const Action: ComposedAction = withDynamicSchemaProps( [onMouseEnter], ); + const handleClick = useMemo(() => { + return ( + onClick && + (async (e, callback) => { + await onClick?.(e, callback); + + // 执行完 onClick 之后,刷新数据区块 + const blocksToRefresh = fieldSchema['x-action-settings']?.onSuccess?.blocksToRefresh || []; + if (blocksToRefresh.length > 0) { + getAllDataBlocks().forEach((block) => { + if (blocksToRefresh.includes(block.uid)) { + try { + block.service?.refresh(); + } catch (error) { + console.error('Failed to refresh block:', block.uid, error); + } + } + }); + } + }) + ); + }, [onClick, fieldSchema, getAllDataBlocks]); + return ( - + + + ); }), { displayName: 'Action' }, @@ -538,6 +571,7 @@ const RenderButtonInner = observer( designerProps: any; title: string; isLink?: boolean; + onlyIcon?: boolean; }) => { const { designable, @@ -559,8 +593,10 @@ const RenderButtonInner = observer( designerProps, title, isLink, + onlyIcon, ...others } = props; + const { t } = useTranslation(); const debouncedClick = useCallback( debounce( (e: React.MouseEvent, checkPortal = true) => { @@ -582,7 +618,8 @@ const RenderButtonInner = observer( return null; } - const actionTitle = title || field?.title; + const rawTitle = title ?? field?.title; + const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle; const { opacity, ...restButtonStyle } = buttonStyle; const linkStyle = isLink && opacity ? { opacity } : undefined; return ( @@ -602,7 +639,7 @@ const RenderButtonInner = observer( type={type === 'danger' ? undefined : type} title={actionTitle} > - {actionTitle && ( + {!onlyIcon && actionTitle && ( {actionTitle} diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx index 09adfde89d..a1dfc5ecea 100644 --- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx +++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx @@ -72,7 +72,7 @@ const InternalActionBar: FC = (props: any) => {
diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts b/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts index d89abc64bb..c903752719 100644 --- a/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts +++ b/packages/core/client/src/schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions.ts @@ -12,9 +12,9 @@ import { useCollection_deprecated, useCollectionFilterOptions } from '../../../. import { useCollectionRecordData } from '../../../../data-source'; import { useTranslation } from 'react-i18next'; import { useCompile } from '../../../'; -import { useBlockContext } from '../../../../block-provider/BlockProvider'; import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks'; import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks'; +import { useFormBlockContext } from '../../../../block-provider'; export const useAfterSuccessOptions = () => { const collection = useCollection_deprecated(); @@ -23,7 +23,7 @@ export const useAfterSuccessOptions = () => { const userFieldOptions = useCollectionFilterOptions('users', 'main'); const compile = useCompile(); const recordData = useCollectionRecordData(); - const { name: blockType } = useBlockContext() || {}; + const { form } = useFormBlockContext(); const [fields, userFields] = useMemo(() => { return [compile(fieldsOptions), compile(userFieldOptions)]; }, [fieldsOptions, userFieldOptions]); @@ -32,7 +32,7 @@ export const useAfterSuccessOptions = () => { const record = useCollectionRecordData(); return useMemo(() => { return [ - (record || blockType === 'form') && { + (record || form) && { value: '$record', label: t('Response record', { ns: 'client' }), children: [...fields], @@ -62,5 +62,5 @@ export const useAfterSuccessOptions = () => { children: null, }, ].filter(Boolean); - }, [recordData, t, fields, blockType, userFields]); + }, [recordData, t, fields, form, userFields]); }; diff --git a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts index 9ce1c8416c..5012fc8d64 100644 --- a/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts +++ b/packages/core/client/src/schema-component/antd/action/hooks/useGetAriaLabelOfAction.ts @@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => { let { name: blockName } = useBlockContext() || {}; const actionTitle = title || compile(fieldSchema.title); collectionName = collectionName ? `-${collectionName}` : ''; - blockName = blockName ? `-${blockName}` : ''; + blockName = blockName && blockName !== 'action' ? `-${blockName}` : ''; action = action ? `-${action}` : ''; recordName = recordName ? `-${recordName}` : ''; diff --git a/packages/core/client/src/schema-component/antd/action/types.ts b/packages/core/client/src/schema-component/antd/action/types.ts index 3df496e1b4..2c319a2ebf 100644 --- a/packages/core/client/src/schema-component/antd/action/types.ts +++ b/packages/core/client/src/schema-component/antd/action/types.ts @@ -92,6 +92,7 @@ export type ActionDrawerProps = T & { footerNodeName?: string; /** 当前弹窗嵌套的层级 */ level?: number; + delay?: number; }; export type ComposedActionDrawer = React.FC> & { diff --git a/packages/core/client/src/schema-component/antd/action/utils.ts b/packages/core/client/src/schema-component/antd/action/utils.ts index 5254021916..216035160a 100644 --- a/packages/core/client/src/schema-component/antd/action/utils.ts +++ b/packages/core/client/src/schema-component/antd/action/utils.ts @@ -87,12 +87,14 @@ export const linkageAction = async ( condition, variables, localVariables, + conditionType, }: { operator; field; condition; variables: VariablesContextType; localVariables: VariableOption[]; + conditionType: 'advanced' | 'basic'; }, jsonLogic: any, ) => { @@ -101,7 +103,7 @@ export const linkageAction = async ( switch (operator) { case ActionType.Visible: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { displayResult.push(operator); field.data = field.data || {}; field.data.hidden = false; @@ -113,7 +115,7 @@ export const linkageAction = async ( field.display = last(displayResult); break; case ActionType.Hidden: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { field.data = field.data || {}; field.data.hidden = true; } else { @@ -122,7 +124,7 @@ export const linkageAction = async ( } break; case ActionType.Disabled: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { disableResult.push(true); } field.stateOfLinkageRules = { @@ -133,7 +135,7 @@ export const linkageAction = async ( field.componentProps['disabled'] = last(disableResult); break; case ActionType.Active: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) { disableResult.push(false); } else { disableResult.push(!!field.componentProps?.['disabled']); diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx index 190a5e6f23..740d66fd5f 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationFieldProvider.tsx @@ -14,6 +14,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useAPIClient, useRequest } from '../../../api-client'; import { useCollectionManager } from '../../../data-source/collection'; import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord'; +import { getDataSourceHeaders } from '../../../data-source/utils'; import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive'; import { useSchemaComponentContext } from '../../hooks'; import { AssociationFieldContext } from './context'; @@ -67,9 +68,11 @@ export const AssociationFieldProvider = observer( if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) { return Promise.reject(null); } + return api.request({ resource: collectionField.target, action: Array.isArray(ids) ? 'list' : 'get', + headers: getDataSourceHeaders(cm?.dataSource?.key), params: { filter: { [targetKey]: ids, diff --git a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx index a2f4741536..ed9969a523 100644 --- a/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx @@ -23,6 +23,7 @@ import { SchemaComponentContext, useAPIClient, useCollectionRecordData, + useCollectionManager_deprecated, } from '../../../'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; import { isVariable } from '../../../variables/utils/isVariable'; @@ -31,6 +32,11 @@ import { Action } from '../action'; import { RemoteSelect, RemoteSelectProps } from '../remote-select'; import useServiceOptions, { useAssociationFieldContext } from './hooks'; +const removeIfKeyEmpty = (obj, filterTargetKey) => { + if (!obj || typeof obj !== 'object' || !filterTargetKey || Array.isArray(obj)) return obj; + return !obj[filterTargetKey] ? null : obj; +}; + export const AssociationFieldAddNewer = (props) => { const schemaComponentCtxValue = useContext(SchemaComponentContext); return ( @@ -93,6 +99,9 @@ const InternalAssociationSelect = observer( const resource = api.resource(collectionField.target); const recordData = useCollectionRecordData(); const schemaComponentCtxValue = useContext(SchemaComponentContext); + const { getCollection } = useCollectionManager_deprecated(); + const associationCollection = getCollection(collectionField.target); + const { filterTargetKey } = associationCollection; useEffect(() => { const initValue = isVariable(field.value) ? undefined : field.value; @@ -167,7 +176,7 @@ const InternalAssociationSelect = observer( {...rest} size={'middle'} objectValue={objectValue} - value={value || innerValue} + value={removeIfKeyEmpty(value || innerValue, filterTargetKey)} service={service} onChange={(value) => { const val = value?.length !== 0 ? value : null; diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx index 1d1be90a8c..b59dec00ad 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalCascadeSelect.tsx @@ -13,6 +13,8 @@ import { FormProvider, connect, createSchemaField, observer, useField, useFieldS import { uid } from '@formily/shared'; import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd'; import dayjs from 'dayjs'; +import { css } from '@emotion/css'; +import { debounce } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAPIClient, useCollectionManager_deprecated } from '../../../'; @@ -152,7 +154,11 @@ const CascadeSelect = connect((props) => { } else { associationField.value = option; } - onChange?.(options); + if (options.length === 1 && !options[0].value) { + onChange?.(null); + } else { + onChange?.(options); + } }; const onDropdownVisibleChange = async (visible, selectedValue, index) => { @@ -238,28 +244,38 @@ export const InternalCascadeSelect = observer( const fieldSchema = useFieldSchema(); const { loading, data: formData } = useDataBlockRequest() || {}; const initialValue = formData?.data?.[fieldSchema.name]; + + const handleFormValuesChange = debounce((form) => { + if (collectionField.interface === 'm2o') { + // 对 m2o 类型字段,提取最后一个非 null 值 + const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]); + setTimeout(() => { + form.setValuesIn(fieldSchema.name, value); + field.value = value; + }); + } else { + // 对 select_array 类型字段,过滤掉空对象 + const value = extractLastNonNullValueObjects(form.values?.select_array).filter( + (v) => v && Object.keys(v).length > 0, + ); + setTimeout(() => { + field.value = value; + }); + } + }, 300); + useEffect(() => { const id = uid(); selectForm.addEffects(id, () => { onFormValuesChange((form) => { - if (collectionField.interface === 'm2o') { - const value = extractLastNonNullValueObjects(form.values?.[fieldSchema.name]); - setTimeout(() => { - form.setValuesIn(fieldSchema.name, value); - field.value = value; - }); - } else { - const value = extractLastNonNullValueObjects(form.values?.select_array).filter( - (v) => v && Object.keys(v).length > 0, - ); - setTimeout(() => { - field.value = value; - }); - } + handleFormValuesChange(form); }); }); + return () => { selectForm.removeEffects(id); + // 清除防抖定时器 + handleFormValuesChange.cancel(); }; }, []); @@ -282,6 +298,24 @@ export const InternalCascadeSelect = observer( items: { type: 'void', 'x-component': 'Space', + 'x-component-props': { + style: { + width: '100%', + display: 'flex', + }, + className: css` + .ant-formily-item-control { + max-width: 100% !important; + } + .ant-space-item:nth-child(1) { + flex: 0.1; + } + + .ant-space-item:nth-child(2) { + flex: 3; + } + `, + }, properties: { sort: { type: 'void', diff --git a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx index 5006ce36ee..032446d4c6 100644 --- a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx @@ -257,8 +257,7 @@ export const SubTable: any = observer( {field.editable && ( { const fieldSchema = useFieldSchema(); const collection = useCollection(); - return React.useMemo(() => collection.getField(fieldSchema.name as any), [fieldSchema.name]); + return React.useMemo(() => collection?.getField(fieldSchema?.name as any), [fieldSchema?.name]); }; export class AssociationFilterPlugin extends Plugin { diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx index e23bfb7b10..a6e0e804ca 100644 --- a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx +++ b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx @@ -9,8 +9,10 @@ import { Card, CardProps } from 'antd'; import React, { useMemo, useRef, useEffect, createContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useToken } from '../../../style'; import { MarkdownReadPretty } from '../markdown'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; export const BlockItemCardContext = createContext({}); @@ -22,6 +24,7 @@ export const BlockItemCard = React.forwardRef(( }, [token.marginBlock]); const [titleHeight, setTitleHeight] = useState(0); const titleRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { const timer = setTimeout(() => { if (titleRef.current) { @@ -38,10 +41,10 @@ export const BlockItemCard = React.forwardRef(( }, [blockTitle, description]); const title = (blockTitle || description) && (
- {blockTitle} + {t(blockTitle, { ns: NAMESPACE_UI_SCHEMA })} {description && ( { let { name: blockName } = useBlockContext() || {}; // eslint-disable-next-line prefer-const let { name: collectionName, getField } = useCollection_deprecated(); - blockName = name || blockName; + blockName = name || (blockName !== 'action' ? blockName : ''); const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title); diff --git a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx index 246d36a7d6..9e711ff07d 100644 --- a/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx +++ b/packages/core/client/src/schema-component/antd/collection-select/__tests__/collection-select.test.tsx @@ -50,11 +50,11 @@ describe('CollectionSelect', () => { >