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..c925ff3fb9 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: @@ -59,7 +61,9 @@ jobs: node-version: ${{ matrix.node_version }} cache: 'yarn' - name: Install project dependencies - run: yarn install + run: | + yarn install + yarn add sqlite3 --no-save -W - name: Test with Sqlite run: yarn test --server --single-thread=false env: diff --git a/.github/workflows/nocobase-test-windows.yml b/.github/workflows/nocobase-test-windows.yml index c1d378a6ba..cd20394a83 100644 --- a/.github/workflows/nocobase-test-windows.yml +++ b/.github/workflows/nocobase-test-windows.yml @@ -63,7 +63,9 @@ jobs: ${{ runner.os }}-yarn- - name: Install project dependencies - run: yarn --prefer-offline + run: | + yarn --prefer-offline + yarn add sqlite3 --no-save -W - name: Test with Sqlite run: yarn test --server --single-thread=false diff --git a/CHANGELOG.md b/CHANGELOG.md index 400e2153da..ade234ae3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,158 @@ 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.24](https://github.com/nocobase/nocobase/compare/v1.6.23...v1.6.24) - 2025-04-24 + +### 🚀 Improvements + +- **[client]** Adjust upload message ([#6757](https://github.com/nocobase/nocobase/pull/6757)) by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** + - only export action in view collection is support when writableView is false ([#6763](https://github.com/nocobase/nocobase/pull/6763)) by @katherinehhh + + - unexpected association data creation when displaying association field under sub-form/sub-table in create form ([#6727](https://github.com/nocobase/nocobase/pull/6727)) by @katherinehhh + + - Incorrect data retrieved for many-to-many array fields from related tables in forms ([#6744](https://github.com/nocobase/nocobase/pull/6744)) by @2013xile + +## [v1.6.23](https://github.com/nocobase/nocobase/compare/v1.6.22...v1.6.23) - 2025-04-23 + +### 🚀 Improvements + +- **[cli]** Optimize internal logic of the `nocobase upgrade` command ([#6754](https://github.com/nocobase/nocobase/pull/6754)) by @chenos + +- **[Template print]** Replaced datasource action control with client role-based access control. by @sheldon66 + +### 🐛 Bug Fixes + +- **[cli]** Auto-update package.json on upgrade ([#6747](https://github.com/nocobase/nocobase/pull/6747)) by @chenos + +- **[client]** + - missing filter for already associated data when adding association data ([#6750](https://github.com/nocobase/nocobase/pull/6750)) by @katherinehhh + + - tree table 'Add Child' button linkage rule missing 'current record' ([#6752](https://github.com/nocobase/nocobase/pull/6752)) by @katherinehhh + +- **[Action: Import records]** Fix the import and export exceptions that occur when setting field permissions. ([#6677](https://github.com/nocobase/nocobase/pull/6677)) by @aaaaaajie + +- **[Block: Gantt]** gantt chart block overlapping months in calendar header for month view ([#6753](https://github.com/nocobase/nocobase/pull/6753)) by @katherinehhh + +- **[Action: Export records Pro]** + - pro export button losing filter parameters after sorting table column by @katherinehhh + + - Fix the import and export exceptions that occur when setting field permissions. by @aaaaaajie + +- **[File storage: S3(Pro)]** Fix response data of uploaded file by @mytharcher + +- **[Workflow: Approval]** Fix preload association fields for records by @mytharcher + +## [v1.6.22](https://github.com/nocobase/nocobase/compare/v1.6.21...v1.6.22) - 2025-04-22 + +### 🚀 Improvements + +- **[create-nocobase-app]** Upgrade dependencies and remove SQLite support ([#6708](https://github.com/nocobase/nocobase/pull/6708)) by @chenos + +- **[File manager]** Expose utils API ([#6705](https://github.com/nocobase/nocobase/pull/6705)) by @mytharcher + +- **[Workflow]** Add date types to variable types set ([#6717](https://github.com/nocobase/nocobase/pull/6717)) by @mytharcher + +### 🐛 Bug Fixes + +- **[client]** + - The problem of mobile top navigation bar icons being difficult to delete ([#6734](https://github.com/nocobase/nocobase/pull/6734)) by @zhangzhonghe + + - After connecting through a foreign key, clicking to trigger filtering results in empty filter conditions ([#6634](https://github.com/nocobase/nocobase/pull/6634)) by @zhangzhonghe + + - picker switching issue in date field of filter button ([#6695](https://github.com/nocobase/nocobase/pull/6695)) by @katherinehhh + + - The issue of the collapse button in the left menu being obscured by the workflow pop-up window ([#6733](https://github.com/nocobase/nocobase/pull/6733)) by @zhangzhonghe + + - missing action option constraints when reopening linkage rules ([#6723](https://github.com/nocobase/nocobase/pull/6723)) by @katherinehhh + + - export button shown without export permission ([#6689](https://github.com/nocobase/nocobase/pull/6689)) by @katherinehhh + + - Required fields hidden by linkage rules should not affect form submission ([#6709](https://github.com/nocobase/nocobase/pull/6709)) by @zhangzhonghe + +- **[server]** appVersion incorrectly generated by create-migration ([#6740](https://github.com/nocobase/nocobase/pull/6740)) by @chenos + +- **[build]** Fix error thrown in tar command ([#6722](https://github.com/nocobase/nocobase/pull/6722)) by @mytharcher + +- **[Workflow]** Fix error thrown when execute schedule event in subflow ([#6721](https://github.com/nocobase/nocobase/pull/6721)) by @mytharcher + +- **[Workflow: Custom action event]** Support to execute in multiple records mode by @mytharcher + +- **[File storage: S3(Pro)]** Add multer make logic for server-side upload by @mytharcher + +## [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 diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index 4dd3bb169d..c6d88eff72 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,158 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +## [v1.6.24](https://github.com/nocobase/nocobase/compare/v1.6.23...v1.6.24) - 2025-04-24 + +### 🚀 优化 + +- **[client]** 调整上传文件的提示信息 ([#6757](https://github.com/nocobase/nocobase/pull/6757)) by @mytharcher + +### 🐛 修复 + +- **[client]** + - 视图表,无编辑权限时允许显示导出按钮 ([#6763](https://github.com/nocobase/nocobase/pull/6763)) by @katherinehhh + + - 新增表单中显示关系字段子表格/子表单时关系数据也被新增 ([#6727](https://github.com/nocobase/nocobase/pull/6727)) by @katherinehhh + + - 在表单中获取关联表中的多对多数组字段数据不正确 ([#6744](https://github.com/nocobase/nocobase/pull/6744)) by @2013xile + +## [v1.6.23](https://github.com/nocobase/nocobase/compare/v1.6.22...v1.6.23) - 2025-04-23 + +### 🚀 优化 + +- **[cli]** 优化 `nocobase upgrade` 命令的内部实现逻辑 ([#6754](https://github.com/nocobase/nocobase/pull/6754)) by @chenos + +- **[模板打印]** 用客户端角色访问控制替换了数据源操作权限控制。 by @sheldon66 + +### 🐛 修复 + +- **[cli]** 升级时自动更新项目的 package.json ([#6747](https://github.com/nocobase/nocobase/pull/6747)) by @chenos + +- **[client]** + - 添加关联表格时未过滤已关联的数据 ([#6750](https://github.com/nocobase/nocobase/pull/6750)) by @katherinehhh + + - 树表格中添加子记录按钮的联动规则缺失「当前记录」变量 ([#6752](https://github.com/nocobase/nocobase/pull/6752)) by @katherinehhh + +- **[操作:导入记录]** 修复设置字段权限时出现的导入导出异常。 ([#6677](https://github.com/nocobase/nocobase/pull/6677)) by @aaaaaajie + +- **[区块:甘特图]** 甘特图区块设置月份视图时,日历头部月份重叠 ([#6753](https://github.com/nocobase/nocobase/pull/6753)) by @katherinehhh + +- **[操作:导出记录 Pro]** + - pro导出按钮在点击表格排序后丢失过滤参数 by @katherinehhh + + - 修复设置字段权限时出现的导入导出异常。 by @aaaaaajie + +- **[文件存储:S3 (Pro)]** 修复已上传文件的响应数据 by @mytharcher + +- **[工作流:审批]** 修复预加载审批记录数据的关系字段 by @mytharcher + +## [v1.6.22](https://github.com/nocobase/nocobase/compare/v1.6.21...v1.6.22) - 2025-04-22 + +### 🚀 优化 + +- **[create-nocobase-app]** 更新依赖,移除 SQLite 支持 ([#6708](https://github.com/nocobase/nocobase/pull/6708)) by @chenos + +- **[文件管理器]** 暴露公共包 API ([#6705](https://github.com/nocobase/nocobase/pull/6705)) by @mytharcher + +- **[工作流]** 为变量的类型集合增加日期相关类型 ([#6717](https://github.com/nocobase/nocobase/pull/6717)) by @mytharcher + +### 🐛 修复 + +- **[client]** + - 移动端顶部的导航栏图标很难被删除的问题 ([#6734](https://github.com/nocobase/nocobase/pull/6734)) by @zhangzhonghe + + - 通过外键连接后,点击触发筛选,筛选条件为空 ([#6634](https://github.com/nocobase/nocobase/pull/6634)) by @zhangzhonghe + + - 筛选按钮中日期字段,切换picker 异常 ([#6695](https://github.com/nocobase/nocobase/pull/6695)) by @katherinehhh + + - 左侧菜单的收起按钮会被绑定工作流弹窗遮挡的问题 ([#6733](https://github.com/nocobase/nocobase/pull/6733)) by @zhangzhonghe + + - 重新打开联动规则时缺少操作选项约束 ([#6723](https://github.com/nocobase/nocobase/pull/6723)) by @katherinehhh + + - 未设置导出权限时仍显示导出按钮 ([#6689](https://github.com/nocobase/nocobase/pull/6689)) by @katherinehhh + + - 被联动规则隐藏的必填字段,不应该影响表单的提交 ([#6709](https://github.com/nocobase/nocobase/pull/6709)) by @zhangzhonghe + +- **[server]** create-migration 命令生成的 appVersion 不准确 ([#6740](https://github.com/nocobase/nocobase/pull/6740)) by @chenos + +- **[build]** 修复 tar 命令报错的问题 ([#6722](https://github.com/nocobase/nocobase/pull/6722)) by @mytharcher + +- **[工作流]** 修复子流程执行定时任务报错的问题 ([#6721](https://github.com/nocobase/nocobase/pull/6721)) by @mytharcher + +- **[工作流:自定义操作事件]** 支持多行记录模式的手动执行 by @mytharcher + +- **[文件存储:S3 (Pro)]** 增加 multer 逻辑用于服务端上传 by @mytharcher + +## [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 ### 🚀 优化 diff --git a/docker/nocobase/Dockerfile b/docker/nocobase/Dockerfile index 5de0d6030a..9c0a1bbfff 100644 --- a/docker/nocobase/Dockerfile +++ b/docker/nocobase/Dockerfile @@ -6,9 +6,13 @@ 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 + && yarn install --production \ + && rm -rf yarn.lock \ + && find node_modules -type f -name "yarn.lock" -delete \ + && find node_modules -type f -name "bower.json" -delete \ + && find node_modules -type f -name "composer.json" -delete RUN cd /app \ && rm -rf nocobase.tar.gz \ diff --git a/lerna.json b/lerna.json index 839cc4ad83..236e219b58 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.6.18", + "version": "1.6.24", "npmClient": "yarn", "useWorkspaces": true, "npmClientArgs": ["--ignore-engines"], diff --git a/package.json b/package.json index 02fefd9951..8dd51ef649 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "ghooks": "^2.0.4", "lint-staged": "^13.2.3", "patch-package": "^8.0.0", + "pm2": "^6.0.5", "pretty-format": "^24.0.0", "pretty-quick": "^3.1.0", "react": "^18.0.0", diff --git a/packages/core/acl/package.json b/packages/core/acl/package.json index 75c2ec6007..578d5054f7 100644 --- a/packages/core/acl/package.json +++ b/packages/core/acl/package.json @@ -1,13 +1,13 @@ { "name": "@nocobase/acl", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/resourcer": "1.6.18", - "@nocobase/utils": "1.6.18", + "@nocobase/resourcer": "1.6.24", + "@nocobase/utils": "1.6.24", "minimatch": "^5.1.1" }, "repository": { diff --git a/packages/core/actions/package.json b/packages/core/actions/package.json index 3292018e85..2dfefcf96e 100644 --- a/packages/core/actions/package.json +++ b/packages/core/actions/package.json @@ -1,14 +1,14 @@ { "name": "@nocobase/actions", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/cache": "1.6.18", - "@nocobase/database": "1.6.18", - "@nocobase/resourcer": "1.6.18" + "@nocobase/cache": "1.6.24", + "@nocobase/database": "1.6.24", + "@nocobase/resourcer": "1.6.24" }, "repository": { "type": "git", diff --git a/packages/core/app/package.json b/packages/core/app/package.json index 2878aeff67..94734b5dda 100644 --- a/packages/core/app/package.json +++ b/packages/core/app/package.json @@ -1,17 +1,17 @@ { "name": "@nocobase/app", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/database": "1.6.18", - "@nocobase/preset-nocobase": "1.6.18", - "@nocobase/server": "1.6.18" + "@nocobase/database": "1.6.24", + "@nocobase/preset-nocobase": "1.6.24", + "@nocobase/server": "1.6.24" }, "devDependencies": { - "@nocobase/client": "1.6.18" + "@nocobase/client": "1.6.24" }, "repository": { "type": "git", diff --git a/packages/core/auth/package.json b/packages/core/auth/package.json index f75eea1fcd..29ae52e9e9 100644 --- a/packages/core/auth/package.json +++ b/packages/core/auth/package.json @@ -1,18 +1,18 @@ { "name": "@nocobase/auth", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", "types": "./lib/index.d.ts", "dependencies": { - "@nocobase/actions": "1.6.18", - "@nocobase/cache": "1.6.18", - "@nocobase/database": "1.6.18", - "@nocobase/resourcer": "1.6.18", - "@nocobase/utils": "1.6.18", - "@types/jsonwebtoken": "^8.5.8", - "jsonwebtoken": "^8.5.1" + "@nocobase/actions": "1.6.24", + "@nocobase/cache": "1.6.24", + "@nocobase/database": "1.6.24", + "@nocobase/resourcer": "1.6.24", + "@nocobase/utils": "1.6.24", + "@types/jsonwebtoken": "^9.0.9", + "jsonwebtoken": "^9.0.2" }, "repository": { "type": "git", diff --git a/packages/core/build/package.json b/packages/core/build/package.json index cc19a756c8..4cc8a1770d 100644 --- a/packages/core/build/package.json +++ b/packages/core/build/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/build", - "version": "1.6.18", + "version": "1.6.24", "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 49fd9908ca..6be2de82f1 100644 --- a/packages/core/cache/package.json +++ b/packages/core/cache/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cache", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./lib/index.js", diff --git a/packages/core/cli/package.json b/packages/core/cli/package.json index ebc416cfc4..cb3d56ba62 100644 --- a/packages/core/cli/package.json +++ b/packages/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/cli", - "version": "1.6.18", + "version": "1.6.24", "description": "", "license": "AGPL-3.0", "main": "./src/index.js", @@ -8,24 +8,25 @@ "nocobase": "./bin/index.js" }, "dependencies": { - "@nocobase/app": "1.6.18", + "@nocobase/app": "1.6.24", "@types/fs-extra": "^11.0.1", "@umijs/utils": "3.5.20", "chalk": "^4.1.1", "commander": "^9.2.0", + "deepmerge": "^4.3.1", "dotenv": "^16.0.0", "execa": "^5.1.1", "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.6.18" + "@nocobase/devtools": "1.6.24" }, "repository": { "type": "git", diff --git a/packages/core/cli/src/commands/dev.js b/packages/core/cli/src/commands/dev.js index c12c5df2d5..9caa52b0ff 100644 --- a/packages/core/cli/src/commands/dev.js +++ b/packages/core/cli/src/commands/dev.js @@ -8,7 +8,7 @@ */ const _ = require('lodash'); const { Command } = require('commander'); -const { generatePlugins, run, postCheck, nodeCheck, promptForTs, isPortReachable } = require('../util'); +const { generatePlugins, run, postCheck, nodeCheck, promptForTs, isPortReachable, checkDBDialect } = require('../util'); const { getPortPromise } = require('portfinder'); const chokidar = require('chokidar'); const { uid } = require('@formily/shared'); @@ -36,6 +36,7 @@ module.exports = (cli) => { .option('-i, --inspect [port]') .allowUnknownOption() .action(async (opts) => { + checkDBDialect(); let subprocess; const runDevClient = () => { console.log('starting client', 1 * clientPort); diff --git a/packages/core/cli/src/commands/e2e.js b/packages/core/cli/src/commands/e2e.js index 8daa5e6389..cf3bf6172a 100644 --- a/packages/core/cli/src/commands/e2e.js +++ b/packages/core/cli/src/commands/e2e.js @@ -8,7 +8,7 @@ */ const { Command } = require('commander'); -const { run, isPortReachable } = require('../util'); +const { run, isPortReachable, checkDBDialect } = require('../util'); const { execSync } = require('node:child_process'); const axios = require('axios'); const { pTest } = require('./p-test'); @@ -165,6 +165,7 @@ const filterArgv = () => { */ module.exports = (cli) => { const e2e = cli.command('e2e').hook('preAction', () => { + checkDBDialect(); if (process.env.APP_BASE_URL) { process.env.APP_BASE_URL = process.env.APP_BASE_URL.replace('localhost', '127.0.0.1'); console.log('APP_BASE_URL:', process.env.APP_BASE_URL); diff --git a/packages/core/cli/src/commands/global.js b/packages/core/cli/src/commands/global.js index 524ad3cd09..48bab4fb23 100644 --- a/packages/core/cli/src/commands/global.js +++ b/packages/core/cli/src/commands/global.js @@ -8,7 +8,7 @@ */ const { Command } = require('commander'); -const { run, isDev, isProd, promptForTs, downloadPro } = require('../util'); +const { run, isDev, isProd, promptForTs, downloadPro, checkDBDialect } = require('../util'); /** * @@ -21,6 +21,7 @@ module.exports = (cli) => { .option('-h, --help') .option('--ts-node-dev') .action(async (options) => { + checkDBDialect(); const cmd = process.argv.slice(2)?.[0]; if (cmd === 'install') { await downloadPro(); diff --git a/packages/core/cli/src/commands/index.js b/packages/core/cli/src/commands/index.js index 3647d07250..97a685f668 100644 --- a/packages/core/cli/src/commands/index.js +++ b/packages/core/cli/src/commands/index.js @@ -30,6 +30,7 @@ module.exports = (cli) => { require('./test')(cli); require('./test-coverage')(cli); require('./umi')(cli); + require('./update-deps')(cli); require('./upgrade')(cli); require('./postinstall')(cli); require('./pkg')(cli); diff --git a/packages/core/cli/src/commands/start.js b/packages/core/cli/src/commands/start.js index 968e8654b2..e0f0bc905c 100644 --- a/packages/core/cli/src/commands/start.js +++ b/packages/core/cli/src/commands/start.js @@ -8,7 +8,7 @@ */ const _ = require('lodash'); const { Command } = require('commander'); -const { run, postCheck, downloadPro, promptForTs } = require('../util'); +const { run, postCheck, downloadPro, promptForTs, checkDBDialect } = require('../util'); const { existsSync, rmSync } = require('fs'); const { resolve, isAbsolute } = require('path'); const chalk = require('chalk'); @@ -48,8 +48,10 @@ module.exports = (cli) => { .option('-i, --instances [instances]') .option('--db-sync') .option('--quickstart') + .option('--launch-mode [launchMode]') .allowUnknownOption() .action(async (opts) => { + checkDBDialect(); if (opts.quickstart) { await downloadPro(); } @@ -118,17 +120,27 @@ module.exports = (cli) => { ]); process.exit(); } else { - run( - 'pm2-runtime', - [ - 'start', - ...instancesArgs, - `${APP_PACKAGE_ROOT}/lib/index.js`, - NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined, - '--', - ...process.argv.slice(2), - ].filter(Boolean), - ); + const launchMode = opts.launchMode || process.env.APP_LAUNCH_MODE || 'pm2'; + if (launchMode === 'pm2') { + run( + 'pm2-runtime', + [ + 'start', + ...instancesArgs, + `${APP_PACKAGE_ROOT}/lib/index.js`, + NODE_ARGS ? `--node-args="${NODE_ARGS}"` : undefined, + '--', + ...process.argv.slice(2), + ].filter(Boolean), + ); + } else { + run( + 'node', + [`${APP_PACKAGE_ROOT}/lib/index.js`, ...(NODE_ARGS || '').split(' '), ...process.argv.slice(2)].filter( + Boolean, + ), + ); + } } }); }; diff --git a/packages/core/cli/src/commands/test-coverage.js b/packages/core/cli/src/commands/test-coverage.js index ee6d50695d..8a26113203 100644 --- a/packages/core/cli/src/commands/test-coverage.js +++ b/packages/core/cli/src/commands/test-coverage.js @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -const { run } = require('../util'); +const { run, checkDBDialect } = require('../util'); const fg = require('fast-glob'); const coreClientPackages = ['packages/core/client', 'packages/core/sdk']; @@ -30,6 +30,7 @@ const getPackagesDir = (isClient) => { module.exports = (cli) => { cli.command('test-coverage:server').action(async () => { + checkDBDialect(); const packageRoots = getPackagesDir(false); for (const dir of packageRoots) { try { @@ -41,6 +42,7 @@ module.exports = (cli) => { }); cli.command('test-coverage:client').action(async () => { + checkDBDialect(); const packageRoots = getPackagesDir(true); for (const dir of packageRoots) { try { diff --git a/packages/core/cli/src/commands/test.js b/packages/core/cli/src/commands/test.js index d97a5f1026..0ec62ac4e8 100644 --- a/packages/core/cli/src/commands/test.js +++ b/packages/core/cli/src/commands/test.js @@ -8,7 +8,7 @@ */ const { Command } = require('commander'); -const { run } = require('../util'); +const { run, checkDBDialect } = require('../util'); const path = require('path'); /** @@ -29,6 +29,7 @@ function addTestCommand(name, cli) { .arguments('[paths...]') .allowUnknownOption() .action(async (paths, opts) => { + checkDBDialect(); if (name === 'test:server') { process.env.TEST_ENV = 'server-side'; } else if (name === 'test:client') { diff --git a/packages/core/cli/src/commands/update-deps.js b/packages/core/cli/src/commands/update-deps.js new file mode 100644 index 0000000000..ed54d06721 --- /dev/null +++ b/packages/core/cli/src/commands/update-deps.js @@ -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. + */ + +const chalk = require('chalk'); +const { Command } = require('commander'); +const { resolve } = require('path'); +const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode, checkDBDialect } = require('../util'); +const { existsSync, rmSync } = require('fs'); +const { readJSON, writeJSON } = require('fs-extra'); +const deepmerge = require('deepmerge'); + +const rmAppDir = () => { + // If ts-node is not installed, do not do the following + const appDevDir = resolve(process.cwd(), './storage/.app-dev'); + if (existsSync(appDevDir)) { + rmSync(appDevDir, { recursive: true, force: true }); + } +}; + +/** + * + * @param {Command} cli + */ +module.exports = (cli) => { + cli + .command('update-deps') + .option('--force') + .allowUnknownOption() + .action(async (options) => { + if (hasCorePackages() || !hasTsNode()) { + await downloadPro(); + return; + } + const pkg = require('../../package.json'); + let distTag = 'latest'; + if (pkg.version.includes('alpha')) { + distTag = 'alpha'; + } else if (pkg.version.includes('beta')) { + distTag = 'beta'; + } + const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], { + stdio: 'pipe', + }); + if (!options.force && pkg.version === stdout) { + await downloadPro(); + rmAppDir(); + return; + } + const descPath = resolve(process.cwd(), 'package.json'); + const descJson = await readJSON(descPath, 'utf8'); + const sourcePath = resolve(__dirname, '../../templates/create-app-package.json'); + const sourceJson = await readJSON(sourcePath, 'utf8'); + if (descJson['dependencies']?.['@nocobase/cli']) { + descJson['dependencies']['@nocobase/cli'] = stdout; + } + if (descJson['devDependencies']?.['@nocobase/devtools']) { + descJson['devDependencies']['@nocobase/devtools'] = stdout; + } + const json = deepmerge(descJson, sourceJson); + await writeJSON(descPath, json, { spaces: 2, encoding: 'utf8' }); + await run('yarn', ['install']); + await downloadPro(); + rmAppDir(); + }); +}; diff --git a/packages/core/cli/src/commands/upgrade.js b/packages/core/cli/src/commands/upgrade.js index 4774564e41..c2bb0736d3 100644 --- a/packages/core/cli/src/commands/upgrade.js +++ b/packages/core/cli/src/commands/upgrade.js @@ -10,15 +10,25 @@ const chalk = require('chalk'); const { Command } = require('commander'); const { resolve } = require('path'); -const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode } = require('../util'); +const { run, promptForTs, runAppCommand, hasCorePackages, downloadPro, hasTsNode, checkDBDialect } = require('../util'); const { existsSync, rmSync } = require('fs'); +const { readJSON, writeJSON } = require('fs-extra'); +const deepmerge = require('deepmerge'); + +async function updatePackage() { + const sourcePath = resolve(__dirname, '../../templates/create-app-package.json'); + const descPath = resolve(process.cwd(), 'package.json'); + const sourceJson = await readJSON(sourcePath, 'utf8'); + const descJson = await readJSON(descPath, 'utf8'); + const json = deepmerge(descJson, sourceJson); + await writeJSON(descPath, json, { spaces: 2, encoding: 'utf8' }); +} /** * * @param {Command} cli */ module.exports = (cli) => { - const { APP_PACKAGE_ROOT } = process.env; cli .command('upgrade') .allowUnknownOption() @@ -26,52 +36,12 @@ module.exports = (cli) => { .option('--next') .option('-S|--skip-code-update') .action(async (options) => { - if (hasTsNode()) promptForTs(); - if (hasCorePackages()) { - // await run('yarn', ['install']); - await downloadPro(); - await runAppCommand('upgrade'); - return; - } + checkDBDialect(); if (options.skipCodeUpdate) { - await downloadPro(); await runAppCommand('upgrade'); - return; + } else { + await run('nocobase', ['update-deps']); + await run('nocobase', ['upgrade', '--skip-code-update']); } - // await runAppCommand('upgrade'); - if (!hasTsNode()) { - await downloadPro(); - await runAppCommand('upgrade'); - return; - } - const rmAppDir = () => { - // If ts-node is not installed, do not do the following - const appDevDir = resolve(process.cwd(), './storage/.app-dev'); - if (existsSync(appDevDir)) { - rmSync(appDevDir, { recursive: true, force: true }); - } - }; - const pkg = require('../../package.json'); - let distTag = 'latest'; - if (pkg.version.includes('alpha')) { - distTag = 'alpha'; - } else if (pkg.version.includes('beta')) { - distTag = 'beta'; - } - // get latest version - const { stdout } = await run('npm', ['info', `@nocobase/cli@${distTag}`, 'version'], { - stdio: 'pipe', - }); - if (pkg.version === stdout) { - await downloadPro(); - await runAppCommand('upgrade'); - await rmAppDir(); - return; - } - await run('yarn', ['add', `@nocobase/cli@${distTag}`, `@nocobase/devtools@${distTag}`, '-W']); - await run('yarn', ['install']); - await downloadPro(); - await runAppCommand('upgrade'); - await rmAppDir(); }); }; diff --git a/packages/core/cli/src/util.js b/packages/core/cli/src/util.js index 742c60cb41..855acd317b 100644 --- a/packages/core/cli/src/util.js +++ b/packages/core/cli/src/util.js @@ -360,7 +360,7 @@ exports.initEnv = function initEnv() { API_BASE_PATH: '/api/', API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_', API_CLIENT_STORAGE_TYPE: 'localStorage', - DB_DIALECT: 'sqlite', + // DB_DIALECT: 'sqlite', DB_STORAGE: 'storage/db/nocobase.sqlite', // DB_TIMEZONE: '+00:00', DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'), @@ -460,8 +460,22 @@ 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.checkDBDialect = function () { + if (!process.env.DB_DIALECT) { + throw new Error('DB_DIALECT is required.'); + } }; exports.generatePlugins = function () { diff --git a/packages/core/cli/templates/create-app-package.json b/packages/core/cli/templates/create-app-package.json new file mode 100644 index 0000000000..3caaa3882e --- /dev/null +++ b/packages/core/cli/templates/create-app-package.json @@ -0,0 +1,39 @@ +{ + "private": true, + "workspaces": ["packages/*/*", "packages/*/*/*"], + "engines": { + "node": ">=18" + }, + "scripts": { + "nocobase": "nocobase", + "pm": "nocobase pm", + "pm2": "nocobase pm2", + "dev": "nocobase dev", + "start": "nocobase start", + "clean": "nocobase clean", + "build": "nocobase build", + "test": "nocobase test", + "e2e": "nocobase e2e", + "tar": "nocobase tar", + "postinstall": "nocobase postinstall", + "lint": "eslint ." + }, + "resolutions": { + "cytoscape": "3.28.0", + "@types/react": "18.3.18", + "@types/react-dom": "^18.0.0", + "react-router-dom": "6.28.1", + "react-router": "6.28.1", + "async": "^3.2.6", + "antd": "5.12.8", + "rollup": "4.24.0", + "semver": "^7.7.1" + }, + "dependencies": { + "pm2": "^6.0.5", + "mysql2": "^3.14.0", + "mariadb": "^2.5.6", + "pg": "^8.14.1", + "pg-hstore": "^2.3.4" + } +} diff --git a/packages/core/client/package.json b/packages/core/client/package.json index a36993bb0d..15e27fb8de 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -1,6 +1,6 @@ { "name": "@nocobase/client", - "version": "1.6.18", + "version": "1.6.24", "license": "AGPL-3.0", "main": "lib/index.js", "module": "es/index.mjs", @@ -27,9 +27,9 @@ "@formily/reactive-react": "^2.2.27", "@formily/shared": "^2.2.27", "@formily/validator": "^2.2.27", - "@nocobase/evaluators": "1.6.18", - "@nocobase/sdk": "1.6.18", - "@nocobase/utils": "1.6.18", + "@nocobase/evaluators": "1.6.24", + "@nocobase/sdk": "1.6.24", + "@nocobase/utils": "1.6.24", "ahooks": "^3.7.2", "antd": "5.12.8", "antd-style": "3.7.1", diff --git a/packages/core/client/src/acl/ACLProvider.tsx b/packages/core/client/src/acl/ACLProvider.tsx index 09eaf964ad..755ed2ee12 100644 --- a/packages/core/client/src/acl/ACLProvider.tsx +++ b/packages/core/client/src/acl/ACLProvider.tsx @@ -309,15 +309,17 @@ 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']; + // 视图表无编辑权限时不支持的操作 + const writableViewCollectionAction = ['create', 'update', 'destroy', 'importXlsx', 'bulkDestroy', 'bulkUpdate']; - 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], @@ -334,16 +336,18 @@ export const ACLActionProvider = (props) => { if (!params) { return {props.children}; } - //视图表无编辑权限时不显示 - if (editablePath.includes(actionPath) || editablePath.includes(actionPath?.split(':')[1])) { + //视图表无编辑权限时不支持 writableViewCollectionAction 的按钮 + if ( + writableViewCollectionAction.includes(actionPath) || + writableViewCollectionAction.includes(actionPath?.split(':')[1]) + ) { if ((collection && collection.template !== 'view') || collection?.writableView) { return {props.children}; } - return null; + return {props.children}; } return {props.children}; }; - export const useACLFieldWhitelist = () => { const params = useContext(ACLActionParamsContext); const whitelist = useMemo(() => { diff --git a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx index f6eacc4ede..1a46420a4b 100644 --- a/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx +++ b/packages/core/client/src/block-provider/FilterFormBlockProvider.tsx @@ -10,11 +10,11 @@ import { useFieldSchema } from '@formily/react'; import React from 'react'; import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; -import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component'; +import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField'; +import { ActionBarProvider, DatePickerProvider, SchemaComponentOptions } from '../schema-component'; import { DefaultValueProvider } from '../schema-settings'; import { CollectOperators } from './CollectOperators'; import { FormBlockProvider } from './FormBlockProvider'; -import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField'; export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { const filedSchema = useFieldSchema(); @@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { }} > false}> - + diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 612064ece0..9f47f4e6c2 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -97,6 +97,30 @@ const filterValue = (value) => { return obj; }; +function getFilteredFormValues(form) { + const values = _.cloneDeep(form.values); + const allFields = []; + form.query('*').forEach((field) => { + if (field) { + allFields.push(field); + } + }); + const readonlyPaths = allFields + .filter((field) => field?.componentProps?.readOnlySubmit) + .map((field) => { + const segments = field.path?.segments || []; + if (segments.length <= 1) { + return segments.join('.'); + } + return segments.slice(0, -1).join('.'); + }); + for (const path of readonlyPaths) { + _.unset(values, path); + } + + return values; +} + export function getFormValues({ filterByTk, field, @@ -124,7 +148,7 @@ export function getFormValues({ } } - return form.values; + return getFilteredFormValues(form); } export function useCollectValuesToSubmit() { @@ -522,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; 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/data-source/data-source/DataSourceManagerProvider.tsx b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx index 547c038e0e..578b447868 100644 --- a/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx +++ b/packages/core/client/src/data-source/data-source/DataSourceManagerProvider.tsx @@ -29,7 +29,7 @@ export function useDataSourceManager() { } /** - * 获取当前 collection 继承链路上的所有 collection + * 获取当前 collection 继承链路上的所有 collection(不包括兄弟表) * @returns */ export function useAllCollectionsInheritChainGetter() { @@ -39,7 +39,7 @@ export function useAllCollectionsInheritChainGetter() { return dm ?.getDataSource(customDataSource) ?.collectionManager?.getCollection(collectionName) - ?.getAllCollectionsInheritChain(); + ?.getInheritChain(); }, [dm], ); diff --git a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts index be25dce192..f8340bd2a5 100644 --- a/packages/core/client/src/filter-provider/__tests__/utiles.test.ts +++ b/packages/core/client/src/filter-provider/__tests__/utiles.test.ts @@ -67,6 +67,108 @@ describe('getSupportFieldsByAssociation', () => { }); describe('getSupportFieldsByForeignKey', () => { + it('should return foreign key fields matching both name and target collection', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collection2' }, + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it("should not return foreign key fields when target collection doesn't match", () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasMany', foreignKey: 'fk2', target: 'collectionX' }, // target不匹配 + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, // 与field2的target不匹配 + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it('should filter out belongsTo type fields', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'belongsTo', foreignKey: 'fk2', target: 'collection2' }, // belongsTo类型 + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'collection3' }, + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, + ]); + }); + + it('should handle when both name and target collection match', () => { + const filterBlockCollection = { + fields: [ + { id: 1, name: 'field1', type: 'hasMany', foreignKey: 'fk1', target: 'collection1' }, + { id: 2, name: 'field2', type: 'hasOne', foreignKey: 'fk2', target: 'collection2' }, + { id: 3, name: 'field3', type: 'hasMany', foreignKey: 'fk3', target: 'wrongCollection' }, // 目标表不匹配 + ], + }; + + const block = { + foreignKeyFields: [ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + { id: 3, name: 'fk3', collectionName: 'collection3' }, // 与field3的target不匹配 + ], + }; + + const result = getSupportFieldsByForeignKey(filterBlockCollection as any, block as any); + + expect(result).toEqual([ + { id: 1, name: 'fk1', collectionName: 'collection1' }, + { id: 2, name: 'fk2', collectionName: 'collection2' }, + ]); + }); + + // 保留原有的通用测试用例 it("should return all foreign key fields matching the filter block collection's foreign key properties", () => { const filterBlockCollection = { fields: [ diff --git a/packages/core/client/src/filter-provider/utils.ts b/packages/core/client/src/filter-provider/utils.ts index 1b3c6920ba..c2759df6ed 100644 --- a/packages/core/client/src/filter-provider/utils.ts +++ b/packages/core/client/src/filter-provider/utils.ts @@ -49,9 +49,13 @@ export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[], export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => { return block.foreignKeyFields?.filter((foreignKeyField) => { - return filterBlockCollection.fields.some( - (field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name, - ); + return filterBlockCollection.fields.some((field) => { + return ( + field.type !== 'belongsTo' && + field.foreignKey === foreignKeyField.name && // 1. 外键字段的 name 要一致 + field.target === foreignKeyField.collectionName // 2. 关系字段的目标表要和外键的数据表一致 + ); + }); }); }; @@ -193,19 +197,21 @@ export const useFilterAPI = () => { const doFilter = useCallback( ( - value: any | ((target: FilterTarget['targets'][0], block: DataBlock) => any), + value: any | ((target: FilterTarget['targets'][0], block: DataBlock, sourceKey?: string) => any), field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id', operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq', ) => { + const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']); dataBlocks.forEach((block) => { + let key = field as string; const target = targets.find((target) => target.uid === block.uid); if (!target) return; if (_.isFunction(value)) { - value = value(target, block); + value = value(target, block, getSourceKey(currentBlock, target.field)); } if (_.isFunction(field)) { - field = field(target, block); + key = field(target, block); } if (_.isFunction(operator)) { operator = operator(target); @@ -219,7 +225,7 @@ export const useFilterAPI = () => { storedFilter[uid] = { $and: [ { - [field]: { + [key]: { [operator]: value, }, }, @@ -248,7 +254,7 @@ export const useFilterAPI = () => { ); }); }, - [dataBlocks, targets, uid], + [dataBlocks, targets, uid, fieldSchema], ); return { @@ -268,3 +274,8 @@ export const isInFilterFormBlock = (fieldSchema: Schema) => { } return false; }; + +function getSourceKey(currentBlock: DataBlock, field: string) { + const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field); + return associationField?.sourceKey || field?.split?.('.')?.[1]; +} diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index f0fb2ebbe9..4da8259285 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -818,7 +818,8 @@ "File size should not exceed {{size}}.": "文件大小不能超过 {{size}}", "File size exceeds the limit": "文件大小超过限制", "File type is not allowed": "文件类型不允许", - "Incomplete uploading files need to be resolved": "未完成上传的文件需要处理", + "Uploading": "上传中", + "Some files are not uploaded correctly, please check.": "部分文件未上传成功,请检查。", "Default title for each record": "用作数据的默认标题", "If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源", "Select an existing piece of data as the initialization data for the form": "选择一条已有的数据作为表单的初始化数据", diff --git a/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx b/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx index 18be2d76b9..5e2a49c7c4 100644 --- a/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx +++ b/packages/core/client/src/modules/actions/associate/AssociateActionProvider.tsx @@ -7,7 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import { RecordPickerProvider, RecordPickerContext } from '../../../schema-component/antd/record-picker'; import { SchemaComponentOptions, @@ -41,9 +41,16 @@ const useTableSelectorProps = () => { export const AssociateActionProvider = (props) => { const [selectedRows, setSelectedRows] = useState([]); const collection = useCollection(); - const { resource, service, block, __parent } = useBlockRequestContext(); + const { resource, block, __parent } = useBlockRequestContext(); const actionCtx = useActionContext(); const { isMobile } = useOpenModeContext() || {}; + const [associationData, setAssociationData] = useState([]); + useEffect(() => { + resource?.list?.().then((res) => { + setAssociationData(res.data?.data || []); + }); + }, [resource]); + const pickerProps = { size: 'small', onChange: props?.onChange, @@ -73,8 +80,8 @@ export const AssociateActionProvider = (props) => { }; const getFilter = () => { const targetKey = collection?.filterTargetKey || 'id'; - if (service.data?.data) { - const list = service.data?.data.map((option) => option[targetKey]).filter(Boolean); + if (associationData) { + const list = associationData.map((option) => option[targetKey]).filter(Boolean); const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {}; return filter; } diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index 352afc0fdb..19bc5794ea 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -306,6 +306,7 @@ test.describe('configure actions column', () => { await page.getByText('Actions', { exact: true }).hover(); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.mouse.move(500, 0); await page.getByText('Actions', { exact: true }).hover(); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); diff --git a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx index a3e45281d1..3a8d26d7ef 100644 --- a/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx +++ b/packages/core/client/src/modules/blocks/filter-blocks/FilterCollectionField.tsx @@ -94,7 +94,11 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => { // @ts-ignore field.dataSource = uiSchema.enum; const originalProps = - compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {}; + compile({ + ...(operator?.schema?.['x-component-props'] || {}), + ...(uiSchema['x-component-props'] || {}), + ...(fieldSchema?.['x-component-props'] || {}), + }) || {}; field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {}); }, [uiSchemaOrigin]); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx new file mode 100644 index 0000000000..5bfd4554d1 --- /dev/null +++ b/packages/core/client/src/route-switch/antd/admin-layout/__tests__/findFirstPageRoute.test.tsx @@ -0,0 +1,212 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { findFirstPageRoute, NocoBaseDesktopRouteType } from '..'; +import { NocoBaseDesktopRoute } from '../convertRoutesToSchema'; + +describe('findFirstPageRoute', () => { + // 基本测试:空路由数组 + it('should return undefined for empty routes array', () => { + const result = findFirstPageRoute([]); + expect(result).toBeUndefined(); + }); + + // 基本测试:undefined 路由数组 + it('should return undefined for undefined routes', () => { + const result = findFirstPageRoute(undefined); + expect(result).toBeUndefined(); + }); + + // 测试:只有一个页面路由 + it('should find the first page route when there is only one page', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0]); + }); + + // 测试:多个页面路由 + it('should find the first page route when there are multiple pages', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + { + id: 2, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0]); + }); + + // 测试:不同类型的路由混合 + it('should find the first page route among mixed route types', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'link1', + type: NocoBaseDesktopRouteType.link, + title: 'Link 1', + }, + { + id: 2, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); + + // 测试:隐藏的菜单项 + it('should ignore hidden menu items', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + hideInMenu: true, + }, + { + id: 2, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); + + // 测试:嵌套路由 + it('should find page route in nested group', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + children: [ + { + id: 11, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0].children[0]); + }); + + // 测试:多层嵌套路由 + it('should find page route in deeply nested groups', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + children: [ + { + id: 11, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1-1', + children: [ + { + id: 111, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[0].children[0].children[0]); + }); + + // 测试:复杂路由结构 + it('should find the first visible page in a complex route structure', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Group 1', + hideInMenu: true, + children: [ + { + id: 11, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ], + }, + { + id: 2, + type: NocoBaseDesktopRouteType.group, + title: 'Group 2', + children: [ + { + id: 21, + schemaUid: 'page2', + type: NocoBaseDesktopRouteType.page, + title: 'Page 2', + }, + ], + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1].children[0]); + }); + + // 测试:空组 + it('should skip empty groups and find page in next group', () => { + const routes: NocoBaseDesktopRoute[] = [ + { + id: 1, + type: NocoBaseDesktopRouteType.group, + title: 'Empty Group', + children: [], + }, + { + id: 2, + schemaUid: 'page1', + type: NocoBaseDesktopRouteType.page, + title: 'Page 1', + }, + ]; + + const result = findFirstPageRoute(routes); + expect(result).toEqual(routes[1]); + }); +}); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx index 0273e6b3b4..bc3fe20fb4 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/index.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/index.tsx @@ -611,27 +611,11 @@ export const InternalAdminLayout = () => { ); }; -function getDefaultPageUid(routes: NocoBaseDesktopRoute[]) { - // Find the first route of type "page" - for (const route of routes) { - if (route.type === NocoBaseDesktopRouteType.page) { - return route.schemaUid; - } - - if (route.children?.length) { - const result = getDefaultPageUid(route.children); - if (result) { - return result; - } - } - } -} - const NavigateToDefaultPage: FC = (props) => { const { allAccessRoutes } = useAllAccessDesktopRoutes(); const location = useLocationNoUpdate(); - const defaultPageUid = getDefaultPageUid(allAccessRoutes); + const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid; return ( <> @@ -741,36 +725,6 @@ export function findRouteBySchemaUid(schemaUid: string, treeArray: any[]) { return null; } -const MenuItemIcon: FC<{ icon: string; title: string }> = (props) => { - const { inHeader } = useContext(headerContext); - - return ( - - {(value: RouteContextType) => { - const { collapsed } = value; - - if (collapsed && !inHeader) { - return props.icon ? ( - - ) : ( - - {props.title.charAt(0)} - - ); - } - - return props.icon ? : null; - }} - - ); -}; - const MenuDesignerButton: FC<{ testId: string }> = (props) => { const { render: renderInitializer } = useSchemaInitializerRender(menuItemInitializer); @@ -892,16 +846,17 @@ function findRouteById(id: string, treeArray: any[]) { return null; } -function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { +export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { if (!routes) return; - for (const route of routes) { + for (const route of routes.filter((item) => !item.hideInMenu)) { if (route.type === NocoBaseDesktopRouteType.page) { return route; } - if (route.children?.length) { - return findFirstPageRoute(route.children); + if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) { + const result = findFirstPageRoute(route.children); + if (result) return result; } } } diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx index 2411b32ed9..0119f4605f 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx @@ -67,21 +67,24 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any } ); export function useDelayedVisible(visible: boolean, delay = 200) { - const [ready, setReady] = useState(false); + const [ready, setReady] = useState(delay === 0); useEffect(() => { + if (ready) { + return; + } if (visible) { const timer = setTimeout(() => setReady(true), delay); return () => clearTimeout(timer); } else { setReady(false); } - }, [visible]); + }, [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(); @@ -102,7 +105,7 @@ export const InternalActionModal: React.FC> = obse } const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0); - const ready = useDelayedVisible(visible, 200); // 200ms 与 Modal 动画时间一致 + const ready = useDelayedVisible(visible, delay); // 200ms 与 Modal 动画时间一致 return ( 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 968cb2ad16..89e9bb5b53 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -321,11 +321,7 @@ const InternalAction: React.FC = observer(function Com(prop } if (addChild) { - return wrapSSR( - - {result} - , - ) as React.ReactElement; + return wrapSSR({result}) as React.ReactElement; } return wrapSSR(result) as React.ReactElement; 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/association-field/AssociationSelect.tsx b/packages/core/client/src/schema-component/antd/association-field/AssociationSelect.tsx index ed9969a523..03c67938c8 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 @@ -34,7 +34,7 @@ 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; + return !obj[filterTargetKey] ? undefined : obj; }; export const AssociationFieldAddNewer = (props) => { @@ -106,8 +106,13 @@ const InternalAssociationSelect = observer( useEffect(() => { const initValue = isVariable(field.value) ? undefined : field.value; const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue; - setInnerValue(value); - }, [field.value]); + const result = removeIfKeyEmpty(value, filterTargetKey); + setInnerValue(result); + if (!isEqual(field.value, result)) { + field.value = result; + } + }, [field.value, filterTargetKey]); + useEffect(() => { const id = uid(); form.addEffects(id, () => { 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 3958aff649..54f4849a7f 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 @@ -54,7 +54,7 @@ describe('CollectionSelect', () => { role="button" >