Merge 'develop' into feat/ai-employee

This commit is contained in:
xilesun 2025-04-24 14:40:12 +08:00
commit 2b5af3f662
435 changed files with 14846 additions and 1982 deletions

View File

@ -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 }}

View File

@ -5,38 +5,40 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
on: on:
push: workflow_dispatch:
branches:
- main # push:
- next # branches:
- develop # - main
paths: # - next
- 'package.json' # - develop
- '**/yarn.lock' # paths:
- 'packages/core/acl/**' # - 'package.json'
- 'packages/core/auth/**' # - '**/yarn.lock'
- 'packages/core/actions/**' # - 'packages/core/acl/**'
- 'packages/core/database/**' # - 'packages/core/auth/**'
- 'packages/core/resourcer/**' # - 'packages/core/actions/**'
- 'packages/core/data-source-manager/**' # - 'packages/core/database/**'
- 'packages/core/server/**' # - 'packages/core/resourcer/**'
- 'packages/core/utils/**' # - 'packages/core/data-source-manager/**'
- 'packages/plugins/**/src/server/**' # - 'packages/core/server/**'
- '.github/workflows/nocobase-test-backend.yml' # - 'packages/core/utils/**'
pull_request: # - 'packages/plugins/**/src/server/**'
paths: # - '.github/workflows/nocobase-test-backend.yml'
- 'package.json' # pull_request:
- '**/yarn.lock' # paths:
- 'packages/core/acl/**' # - 'package.json'
- 'packages/core/auth/**' # - '**/yarn.lock'
- 'packages/core/actions/**' # - 'packages/core/acl/**'
- 'packages/core/database/**' # - 'packages/core/auth/**'
- 'packages/core/resourcer/**' # - 'packages/core/actions/**'
- 'packages/core/data-source-manager/**' # - 'packages/core/database/**'
- 'packages/core/server/**' # - 'packages/core/resourcer/**'
- 'packages/core/utils/**' # - 'packages/core/data-source-manager/**'
- 'packages/plugins/**/src/server/**' # - 'packages/core/server/**'
- '.github/workflows/nocobase-test-backend.yml' # - 'packages/core/utils/**'
# - 'packages/plugins/**/src/server/**'
# - '.github/workflows/nocobase-test-backend.yml'
jobs: jobs:
sqlite-test: sqlite-test:
@ -59,7 +61,9 @@ jobs:
node-version: ${{ matrix.node_version }} node-version: ${{ matrix.node_version }}
cache: 'yarn' cache: 'yarn'
- name: Install project dependencies - name: Install project dependencies
run: yarn install run: |
yarn install
yarn add sqlite3 --no-save -W
- name: Test with Sqlite - name: Test with Sqlite
run: yarn test --server --single-thread=false run: yarn test --server --single-thread=false
env: env:

View File

@ -63,7 +63,9 @@ jobs:
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- name: Install project dependencies - name: Install project dependencies
run: yarn --prefer-offline run: |
yarn --prefer-offline
yarn add sqlite3 --no-save -W
- name: Test with Sqlite - name: Test with Sqlite
run: yarn test --server --single-thread=false run: yarn test --server --single-thread=false

View File

@ -5,6 +5,167 @@ 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/) 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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
- **[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 ## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
### 🚀 Improvements ### 🚀 Improvements

View File

@ -5,6 +5,167 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [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
### 🚀 优化
- **[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 ## [v1.6.17](https://github.com/nocobase/nocobase/compare/v1.6.16...v1.6.17) - 2025-04-09
### 🚀 优化 ### 🚀 优化

View File

@ -6,9 +6,13 @@ WORKDIR /app
RUN cd /app \ RUN cd /app \
&& yarn config set network-timeout 600000 -g \ && 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 \ && 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 \ RUN cd /app \
&& rm -rf nocobase.tar.gz \ && rm -rf nocobase.tar.gz \

View File

@ -1,5 +1,5 @@
{ {
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

@ -83,6 +83,7 @@
"ghooks": "^2.0.4", "ghooks": "^2.0.4",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"pm2": "^6.0.5",
"pretty-format": "^24.0.0", "pretty-format": "^24.0.0",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"react": "^18.0.0", "react": "^18.0.0",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/acl", "name": "@nocobase/acl",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/resourcer": "1.7.0-beta.16", "@nocobase/resourcer": "1.7.0-alpha.12",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-alpha.12",
"minimatch": "^5.1.1" "minimatch": "^5.1.1"
}, },
"repository": { "repository": {

View File

@ -1,14 +1,14 @@
{ {
"name": "@nocobase/actions", "name": "@nocobase/actions",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/cache": "1.7.0-beta.16", "@nocobase/cache": "1.7.0-alpha.12",
"@nocobase/database": "1.7.0-beta.16", "@nocobase/database": "1.7.0-alpha.12",
"@nocobase/resourcer": "1.7.0-beta.16" "@nocobase/resourcer": "1.7.0-alpha.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/database": "1.7.0-beta.16", "@nocobase/database": "1.7.0-alpha.12",
"@nocobase/preset-nocobase": "1.7.0-beta.16", "@nocobase/preset-nocobase": "1.7.0-alpha.12",
"@nocobase/server": "1.7.0-beta.16" "@nocobase/server": "1.7.0-alpha.12"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "1.7.0-beta.16" "@nocobase/client": "1.7.0-alpha.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,18 +1,18 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "1.7.0-beta.16", "@nocobase/actions": "1.7.0-alpha.12",
"@nocobase/cache": "1.7.0-beta.16", "@nocobase/cache": "1.7.0-alpha.12",
"@nocobase/database": "1.7.0-beta.16", "@nocobase/database": "1.7.0-alpha.12",
"@nocobase/resourcer": "1.7.0-beta.16", "@nocobase/resourcer": "1.7.0-alpha.12",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-alpha.12",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^9.0.9",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^9.0.2"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/build", "name": "@nocobase/build",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "Library build tool based on rollup.", "description": "Library build tool based on rollup.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -17,7 +17,7 @@
"@lerna/project": "4.0.0", "@lerna/project": "4.0.0",
"@rsbuild/plugin-babel": "^1.0.3", "@rsbuild/plugin-babel": "^1.0.3",
"@rsdoctor/rspack-plugin": "^0.4.8", "@rsdoctor/rspack-plugin": "^0.4.8",
"@rspack/core": "1.1.1", "@rspack/core": "1.3.2",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@types/gulp": "^4.0.13", "@types/gulp": "^4.0.13",
"@types/lerna__package": "5.1.0", "@types/lerna__package": "5.1.0",
@ -39,7 +39,7 @@
"postcss-preset-env": "^9.1.2", "postcss-preset-env": "^9.1.2",
"react-imported-component": "^6.5.4", "react-imported-component": "^6.5.4",
"style-loader": "^3.3.3", "style-loader": "^3.3.3",
"tar": "^6.2.0", "tar": "^7.4.3",
"tsup": "8.2.4", "tsup": "8.2.4",
"typescript": "5.1.3", "typescript": "5.1.3",
"update-notifier": "3.0.0", "update-notifier": "3.0.0",

View File

@ -347,6 +347,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
umdNamedDefine: true, umdNamedDefine: true,
}, },
}, },
amd: {},
resolve: { resolve: {
tsConfig: path.join(process.cwd(), 'tsconfig.json'), tsConfig: path.join(process.cwd(), 'tsconfig.json'),
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'], extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.less', '.css'],

View File

@ -8,7 +8,7 @@
*/ */
import path from 'path'; import path from 'path';
import tar from 'tar'; import { create } from 'tar';
import fg from 'fast-glob'; import fg from 'fast-glob';
import fs from 'fs-extra'; import fs from 'fs-extra';
@ -38,5 +38,5 @@ export function tarPlugin(cwd: string, log: PkgLog) {
fs.mkdirpSync(path.dirname(tarball)); fs.mkdirpSync(path.dirname(tarball));
fs.rmSync(tarball, { force: true }); fs.rmSync(tarball, { force: true });
return tar.c({ gzip: true, file: tarball, cwd }, tarFiles); return create({ gzip: true, file: tarball, cwd }, tarFiles);
} }

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/lock-manager": "1.7.0-beta.16", "@nocobase/lock-manager": "1.7.0-alpha.12",
"bloom-filters": "^3.0.1", "bloom-filters": "^3.0.1",
"cache-manager": "^5.2.4", "cache-manager": "^5.2.4",
"cache-manager-redis-yet": "^4.1.2" "cache-manager-redis-yet": "^4.1.2"

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,24 +8,25 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "1.7.0-beta.16", "@nocobase/app": "1.7.0-alpha.12",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"commander": "^9.2.0", "commander": "^9.2.0",
"deepmerge": "^4.3.1",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"execa": "^5.1.1", "execa": "^5.1.1",
"fast-glob": "^3.3.1", "fast-glob": "^3.3.1",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"p-all": "3.0.0", "p-all": "3.0.0",
"pm2": "^5.2.0", "pm2": "^6.0.5",
"portfinder": "^1.0.28", "portfinder": "^1.0.28",
"serve": "^13.0.2", "tar": "^7.4.3",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"tsx": "^4.19.0" "tsx": "^4.19.0"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "1.7.0-beta.16" "@nocobase/devtools": "1.7.0-alpha.12"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -8,7 +8,7 @@
*/ */
const _ = require('lodash'); const _ = require('lodash');
const { Command } = require('commander'); 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 { getPortPromise } = require('portfinder');
const chokidar = require('chokidar'); const chokidar = require('chokidar');
const { uid } = require('@formily/shared'); const { uid } = require('@formily/shared');
@ -36,6 +36,7 @@ module.exports = (cli) => {
.option('-i, --inspect [port]') .option('-i, --inspect [port]')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
checkDBDialect();
let subprocess; let subprocess;
const runDevClient = () => { const runDevClient = () => {
console.log('starting client', 1 * clientPort); console.log('starting client', 1 * clientPort);

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run, isPortReachable } = require('../util'); const { run, isPortReachable, checkDBDialect } = require('../util');
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');
const axios = require('axios'); const axios = require('axios');
const { pTest } = require('./p-test'); const { pTest } = require('./p-test');
@ -165,6 +165,7 @@ const filterArgv = () => {
*/ */
module.exports = (cli) => { module.exports = (cli) => {
const e2e = cli.command('e2e').hook('preAction', () => { const e2e = cli.command('e2e').hook('preAction', () => {
checkDBDialect();
if (process.env.APP_BASE_URL) { if (process.env.APP_BASE_URL) {
process.env.APP_BASE_URL = process.env.APP_BASE_URL.replace('localhost', '127.0.0.1'); 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); console.log('APP_BASE_URL:', process.env.APP_BASE_URL);

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); 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('-h, --help')
.option('--ts-node-dev') .option('--ts-node-dev')
.action(async (options) => { .action(async (options) => {
checkDBDialect();
const cmd = process.argv.slice(2)?.[0]; const cmd = process.argv.slice(2)?.[0];
if (cmd === 'install') { if (cmd === 'install') {
await downloadPro(); await downloadPro();

View File

@ -30,6 +30,7 @@ module.exports = (cli) => {
require('./test')(cli); require('./test')(cli);
require('./test-coverage')(cli); require('./test-coverage')(cli);
require('./umi')(cli); require('./umi')(cli);
require('./update-deps')(cli);
require('./upgrade')(cli); require('./upgrade')(cli);
require('./postinstall')(cli); require('./postinstall')(cli);
require('./pkg')(cli); require('./pkg')(cli);

View File

@ -8,7 +8,7 @@
*/ */
const _ = require('lodash'); const _ = require('lodash');
const { Command } = require('commander'); 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 { existsSync, rmSync } = require('fs');
const { resolve, isAbsolute } = require('path'); const { resolve, isAbsolute } = require('path');
const chalk = require('chalk'); const chalk = require('chalk');
@ -48,8 +48,10 @@ module.exports = (cli) => {
.option('-i, --instances [instances]') .option('-i, --instances [instances]')
.option('--db-sync') .option('--db-sync')
.option('--quickstart') .option('--quickstart')
.option('--launch-mode [launchMode]')
.allowUnknownOption() .allowUnknownOption()
.action(async (opts) => { .action(async (opts) => {
checkDBDialect();
if (opts.quickstart) { if (opts.quickstart) {
await downloadPro(); await downloadPro();
} }
@ -118,6 +120,8 @@ module.exports = (cli) => {
]); ]);
process.exit(); process.exit();
} else { } else {
const launchMode = opts.launchMode || process.env.APP_LAUNCH_MODE || 'pm2';
if (launchMode === 'pm2') {
run( run(
'pm2-runtime', 'pm2-runtime',
[ [
@ -129,6 +133,14 @@ module.exports = (cli) => {
...process.argv.slice(2), ...process.argv.slice(2),
].filter(Boolean), ].filter(Boolean),
); );
} else {
run(
'node',
[`${APP_PACKAGE_ROOT}/lib/index.js`, ...(NODE_ARGS || '').split(' '), ...process.argv.slice(2)].filter(
Boolean,
),
);
}
} }
}); });
}; };

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 fg = require('fast-glob');
const coreClientPackages = ['packages/core/client', 'packages/core/sdk']; const coreClientPackages = ['packages/core/client', 'packages/core/sdk'];
@ -30,6 +30,7 @@ const getPackagesDir = (isClient) => {
module.exports = (cli) => { module.exports = (cli) => {
cli.command('test-coverage:server').action(async () => { cli.command('test-coverage:server').action(async () => {
checkDBDialect();
const packageRoots = getPackagesDir(false); const packageRoots = getPackagesDir(false);
for (const dir of packageRoots) { for (const dir of packageRoots) {
try { try {
@ -41,6 +42,7 @@ module.exports = (cli) => {
}); });
cli.command('test-coverage:client').action(async () => { cli.command('test-coverage:client').action(async () => {
checkDBDialect();
const packageRoots = getPackagesDir(true); const packageRoots = getPackagesDir(true);
for (const dir of packageRoots) { for (const dir of packageRoots) {
try { try {

View File

@ -8,7 +8,7 @@
*/ */
const { Command } = require('commander'); const { Command } = require('commander');
const { run } = require('../util'); const { run, checkDBDialect } = require('../util');
const path = require('path'); const path = require('path');
/** /**
@ -29,6 +29,7 @@ function addTestCommand(name, cli) {
.arguments('[paths...]') .arguments('[paths...]')
.allowUnknownOption() .allowUnknownOption()
.action(async (paths, opts) => { .action(async (paths, opts) => {
checkDBDialect();
if (name === 'test:server') { if (name === 'test:server') {
process.env.TEST_ENV = 'server-side'; process.env.TEST_ENV = 'server-side';
} else if (name === 'test:client') { } else if (name === 'test:client') {

View File

@ -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();
});
};

View File

@ -10,15 +10,25 @@
const chalk = require('chalk'); const chalk = require('chalk');
const { Command } = require('commander'); const { Command } = require('commander');
const { resolve } = require('path'); 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 { 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 * @param {Command} cli
*/ */
module.exports = (cli) => { module.exports = (cli) => {
const { APP_PACKAGE_ROOT } = process.env;
cli cli
.command('upgrade') .command('upgrade')
.allowUnknownOption() .allowUnknownOption()
@ -26,52 +36,12 @@ module.exports = (cli) => {
.option('--next') .option('--next')
.option('-S|--skip-code-update') .option('-S|--skip-code-update')
.action(async (options) => { .action(async (options) => {
if (hasTsNode()) promptForTs(); checkDBDialect();
if (hasCorePackages()) {
// await run('yarn', ['install']);
await downloadPro();
await runAppCommand('upgrade');
return;
}
if (options.skipCodeUpdate) { if (options.skipCodeUpdate) {
await downloadPro();
await runAppCommand('upgrade'); 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();
}); });
}; };

View File

@ -360,7 +360,7 @@ exports.initEnv = function initEnv() {
API_BASE_PATH: '/api/', API_BASE_PATH: '/api/',
API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_', API_CLIENT_STORAGE_PREFIX: 'NOCOBASE_',
API_CLIENT_STORAGE_TYPE: 'localStorage', API_CLIENT_STORAGE_TYPE: 'localStorage',
DB_DIALECT: 'sqlite', // DB_DIALECT: 'sqlite',
DB_STORAGE: 'storage/db/nocobase.sqlite', DB_STORAGE: 'storage/db/nocobase.sqlite',
// DB_TIMEZONE: '+00:00', // DB_TIMEZONE: '+00:00',
DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'), DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'),
@ -460,8 +460,22 @@ exports.initEnv = function initEnv() {
process.env.SOCKET_PATH = generateGatewayPath(); process.env.SOCKET_PATH = generateGatewayPath();
fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true }); fs.mkdirpSync(dirname(process.env.SOCKET_PATH), { force: true, recursive: true });
fs.mkdirpSync(process.env.PM2_HOME, { 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'); const pkgs = [
fs.existsSync(pkgDir) && fs.rmdirSync(pkgDir, { force: true }); '@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 () { exports.generatePlugins = function () {

View File

@ -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": "^3.4.1",
"pg": "^8.14.1",
"pg-hstore": "^2.3.4"
}
}

View File

@ -234,6 +234,10 @@ export default defineConfig({
"title": "Filter", "title": "Filter",
"link": "/components/filter" "link": "/components/filter"
}, },
{
"title": "LinkageFilter",
"link": "/components/linkage-filter"
},
] ]
}, },
{ {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "1.7.0-beta.16", "version": "1.7.0-alpha.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "lib/index.js", "main": "lib/index.js",
"module": "es/index.mjs", "module": "es/index.mjs",
@ -27,9 +27,9 @@
"@formily/reactive-react": "^2.2.27", "@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27", "@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27", "@formily/validator": "^2.2.27",
"@nocobase/evaluators": "1.7.0-beta.16", "@nocobase/evaluators": "1.7.0-alpha.12",
"@nocobase/sdk": "1.7.0-beta.16", "@nocobase/sdk": "1.7.0-alpha.12",
"@nocobase/utils": "1.7.0-beta.16", "@nocobase/utils": "1.7.0-alpha.12",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "5.24.2", "antd": "5.24.2",
"antd-style": "3.7.1", "antd-style": "3.7.1",

View File

@ -314,15 +314,15 @@ export const ACLActionProvider = (props) => {
const schema = useFieldSchema(); const schema = useFieldSchema();
const currentUid = schema['x-uid']; const currentUid = schema['x-uid'];
let actionPath = schema['x-acl-action']; 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']}`; actionPath = `${resource}:${schema['x-action']}`;
} }
if (actionPath && !actionPath?.includes(':')) { if (actionPath && !actionPath?.includes(':')) {
actionPath = `${resource}:${actionPath}`; actionPath = `${resource}:${actionPath}`;
} }
const params = useMemo( const params = useMemo(
() => actionPath && parseAction(actionPath, { schema, recordPkValue }), () => actionPath && parseAction(actionPath, { schema, recordPkValue }),
[parseAction, actionPath, schema, recordPkValue], [parseAction, actionPath, schema, recordPkValue],
@ -340,7 +340,7 @@ export const ACLActionProvider = (props) => {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>; return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
} }
//视图表无编辑权限时不显示 //视图表无编辑权限时不显示
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) { if ((collection && collection.template !== 'view') || collection?.writableView) {
return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>; return <ACLActionParamsContext.Provider value={params}>{props.children}</ACLActionParamsContext.Provider>;
} }

View File

@ -167,7 +167,7 @@ export function getOperators() {
const dateA = parseDate(a); const dateA = parseDate(a);
const dateB = parseDate(b); const dateB = parseDate(b);
if (!dateA || !dateB) { if (!dateA || !dateB) {
throw new Error('Invalid date format'); return false;
} }
return dateA < dateB; return dateA < dateB;
}, },
@ -651,10 +651,11 @@ function parseYear(dateStr) {
} }
function parseDate(targetDateStr) { function parseDate(targetDateStr) {
let dateStr = Array.isArray(targetDateStr) ? targetDateStr[1] : targetDateStr; let dateStr = Array.isArray(targetDateStr) ? targetDateStr[1] ?? targetDateStr[0] : targetDateStr;
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(dateStr)) { if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(dateStr)) {
// ISO 8601 格式YYYY-MM-DDTHH:mm:ss.sssZ return new Date(dateStr);
return new Date(dateStr); // 直接解析为 Date 对象 } else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(dateStr)) {
return new Date(dateStr.replace(' ', 'T'));
} else if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { } else if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
// YYYY-MM-DD 格式 // YYYY-MM-DD 格式
return parseFullDate(dateStr); return parseFullDate(dateStr);
@ -668,5 +669,6 @@ function parseDate(targetDateStr) {
// YYYY 格式 // YYYY 格式
return parseYear(dateStr); return parseYear(dateStr);
} }
return null; // Invalid format
return null;
} }

View File

@ -10,11 +10,11 @@
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import React from 'react'; import React from 'react';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps'; 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 { DefaultValueProvider } from '../schema-settings';
import { CollectOperators } from './CollectOperators'; import { CollectOperators } from './CollectOperators';
import { FormBlockProvider } from './FormBlockProvider'; import { FormBlockProvider } from './FormBlockProvider';
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => { export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const filedSchema = useFieldSchema(); const filedSchema = useFieldSchema();
@ -35,7 +35,7 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
}} }}
> >
<DefaultValueProvider isAllowToSetDefaultValue={() => false}> <DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider> <FormBlockProvider name="filter-form" {...props} confirmBeforeClose={false}></FormBlockProvider>
</DefaultValueProvider> </DefaultValueProvider>
</ActionBarProvider> </ActionBarProvider>
</DatePickerProvider> </DatePickerProvider>

View File

@ -97,6 +97,30 @@ const filterValue = (value) => {
return obj; 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({ export function getFormValues({
filterByTk, filterByTk,
field, field,
@ -124,7 +148,7 @@ export function getFormValues({
} }
} }
return form.values; return getFilteredFormValues(form);
} }
export function useCollectValuesToSubmit() { export function useCollectValuesToSubmit() {
@ -546,9 +570,11 @@ export const useFilterBlockActionProps = () => {
const { doFilter } = useDoFilter(); const { doFilter } = useDoFilter();
const actionField = useField(); const actionField = useField();
actionField.data = actionField.data || {}; actionField.data = actionField.data || {};
const form = useForm();
return { return {
async onClick() { async onClick() {
await form.submit();
actionField.data.loading = true; actionField.data.loading = true;
await doFilter(); await doFilter();
actionField.data.loading = false; actionField.data.loading = false;
@ -1580,7 +1606,7 @@ export const getAppends = ({
const fieldNames = getTargetField(item); const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段 // 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) { if (fieldNames.length > 1 && !item.op) {
appends.add(fieldNames.join('.')); appends.add(fieldNames.join('.'));
} }
}); });

View File

@ -7,9 +7,9 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 type { CollectionFieldOptions, GetCollectionFieldPredicate } from '../../data-source';
import { Collection } from '../../data-source/collection/Collection'; import { Collection } from '../../data-source/collection/Collection';
import _, { filter, unionBy, uniq } from 'lodash';
export class InheritanceCollectionMixin extends Collection { export class InheritanceCollectionMixin extends Collection {
protected parentCollectionsName: string[]; protected parentCollectionsName: string[];
@ -22,6 +22,7 @@ export class InheritanceCollectionMixin extends Collection {
protected parentCollectionFields: Record<string, CollectionFieldOptions[]> = {}; protected parentCollectionFields: Record<string, CollectionFieldOptions[]> = {};
protected allCollectionsInheritChain: string[]; protected allCollectionsInheritChain: string[];
protected inheritCollectionsChain: string[]; protected inheritCollectionsChain: string[];
protected inheritChain: string[];
protected foreignKeyFields: CollectionFieldOptions[]; protected foreignKeyFields: CollectionFieldOptions[];
getParentCollectionsName() { getParentCollectionsName() {
@ -233,6 +234,43 @@ export class InheritanceCollectionMixin extends Collection {
return this.inheritCollectionsChain; 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<InheritanceCollectionMixin>(name);
if (childCollection) {
// 递归添加每个后代的后代
const childrenNames = childCollection.getChildrenCollectionsName();
addDescendants(childrenNames);
}
}
}
};
// 从当前集合的直接后代开始添加
addDescendants(descendantNames);
this.inheritChain = inheritChain;
return this.inheritChain;
}
getAllFields(predicate?: GetCollectionFieldPredicate) { getAllFields(predicate?: GetCollectionFieldPredicate) {
if (this.allFields) { if (this.allFields) {
return this.allFields.slice(); return this.allFields.slice();

View File

@ -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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('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<InheritanceCollectionMixin>('child');
const grandChild = collectionManager.getCollection<InheritanceCollectionMixin>('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);
});
});
});

View File

@ -29,7 +29,7 @@ export function useDataSourceManager() {
} }
/** /**
* collection collection * collection collection
* @returns * @returns
*/ */
export function useAllCollectionsInheritChainGetter() { export function useAllCollectionsInheritChainGetter() {
@ -39,7 +39,7 @@ export function useAllCollectionsInheritChainGetter() {
return dm return dm
?.getDataSource(customDataSource) ?.getDataSource(customDataSource)
?.collectionManager?.getCollection<InheritanceCollectionMixin>(collectionName) ?.collectionManager?.getCollection<InheritanceCollectionMixin>(collectionName)
?.getAllCollectionsInheritChain(); ?.getInheritChain();
}, },
[dm], [dm],
); );

View File

@ -19,6 +19,7 @@ import { mergeFilter, useAssociatedFields } from './utils';
// @ts-ignore // @ts-ignore
import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'; import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
import { useAllDataBlocks } from '../schema-component/antd/page/AllDataBlocksProvider';
enum FILTER_OPERATOR { enum FILTER_OPERATOR {
AND = '$and', AND = '$and',
@ -71,6 +72,10 @@ export interface DataBlock {
* manual: 只有当点击了筛选按钮 * manual: 只有当点击了筛选按钮
*/ */
dataLoadingMode?: 'auto' | 'manual'; dataLoadingMode?: 'auto' | 'manual';
/** 让整个区块悬浮起来 */
highlightBlock: () => void;
/** 取消悬浮 */
unhighlightBlock: () => void;
} }
interface FilterContextValue { interface FilterContextValue {
@ -124,7 +129,7 @@ export const DataBlockCollector = ({
const field = useField(); const field = useField();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const associatedFields = useAssociatedFields(); const associatedFields = useAssociatedFields();
const container = useRef(null); const container = useRef<HTMLDivElement | null>(null);
const dataLoadingMode = useDataLoadingMode(); const dataLoadingMode = useDataLoadingMode();
const shouldApplyFilter = const shouldApplyFilter =
@ -172,6 +177,34 @@ export const DataBlockCollector = ({
field.data?.clearSelectedRowKeys?.(); field.data?.clearSelectedRowKeys?.();
} }
}, },
highlightBlock() {
const dom = container.current;
if (!dom) return;
const designer = dom.querySelector('.ant-nb-schema-toolbar');
if (designer) {
designer.classList.remove(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
}
dom.style.boxShadow = '0 3px 12px rgba(0, 0, 0, 0.15)';
dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
dom.scrollIntoView?.({
behavior: 'smooth',
block: 'start',
});
},
unhighlightBlock() {
const dom = container.current;
if (!dom) return;
const designer = dom.querySelector('.ant-nb-schema-toolbar');
if (designer) {
designer.classList.add(process.env.__E2E__ ? 'hidden-e2e' : 'hidden');
}
dom.style.boxShadow = 'none';
dom.style.transition = 'box-shadow 0.3s ease, transform 0.2s ease';
}
}); });
}, [ }, [
associatedFields, associatedFields,
@ -197,12 +230,14 @@ export const DataBlockCollector = ({
*/ */
export const useFilterBlock = () => { export const useFilterBlock = () => {
const ctx = React.useContext(FilterContext); const ctx = React.useContext(FilterContext);
const allDataBlocksCtx = useAllDataBlocks();
// 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面 // 有可能存在页面没有提供 FilterBlockProvider 的情况,比如内部使用的数据表管理页面
const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.getDataBlocks() || [], [ctx]); const getDataBlocks = useCallback<() => DataBlock[]>(() => ctx?.getDataBlocks() || [], [ctx]);
const recordDataBlocks = useCallback( const recordDataBlocks = useCallback(
(block: DataBlock) => { (block: DataBlock) => {
allDataBlocksCtx.recordDataBlocks(block);
const existingBlock = ctx?.getDataBlocks().find((item) => item.uid === block.uid); const existingBlock = ctx?.getDataBlocks().find((item) => item.uid === block.uid);
if (existingBlock) { if (existingBlock) {
@ -218,6 +253,7 @@ export const useFilterBlock = () => {
const removeDataBlock = useCallback( const removeDataBlock = useCallback(
(uid: string) => { (uid: string) => {
allDataBlocksCtx.removeDataBlock(uid);
if (ctx?.getDataBlocks().every((item) => item.uid !== uid)) return; if (ctx?.getDataBlocks().every((item) => item.uid !== uid)) return;
ctx?.setDataBlocks((prev) => prev.filter((item) => item.uid !== uid)); ctx?.setDataBlocks((prev) => prev.filter((item) => item.uid !== uid));
}, },

View File

@ -67,6 +67,108 @@ describe('getSupportFieldsByAssociation', () => {
}); });
describe('getSupportFieldsByForeignKey', () => { 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", () => { it("should return all foreign key fields matching the filter block collection's foreign key properties", () => {
const filterBlockCollection = { const filterBlockCollection = {
fields: [ fields: [

View File

@ -0,0 +1,47 @@
let container: HTMLElement | null = null;
export const highlightBlock = (clonedBlockDom: HTMLElement, boxRect: DOMRect) => {
if (!container) {
container = document.createElement('div');
document.body.appendChild(container);
container.style.position = 'absolute';
container.style.transition = 'opacity 0.3s ease';
container.style.pointerEvents = 'none';
}
container.appendChild(clonedBlockDom);
container.style.opacity = '1';
container.style.width = `${boxRect.width}px`;
container.style.height = `${boxRect.height}px`;
container.style.top = `${boxRect.top}px`;
container.style.left = `${boxRect.left}px`;
container.style.zIndex = '2000';
}
export const unhighlightBlock = () => {
if (container) {
container.style.opacity = '0';
container.innerHTML = '';
}
}
export const startScrollEndTracking = (dom: HTMLElement & { _prevRect?: DOMRect; _timer?: any }, callback: () => void) => {
dom._timer = setInterval(() => {
const prevRect = dom._prevRect;
const currentRect = dom.getBoundingClientRect();
if (!prevRect || currentRect.top !== prevRect.top) {
dom._prevRect = currentRect;
} else {
clearInterval(dom._timer);
callback();
}
}, 100)
}
export const stopScrollEndTracking = (dom: HTMLElement & { _timer?: any }) => {
if (dom._timer) {
clearInterval(dom._timer);
dom._timer = null;
}
}

View File

@ -49,10 +49,14 @@ export const getSupportFieldsByAssociation = (inheritCollectionsChain: string[],
export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => { export const getSupportFieldsByForeignKey = (filterBlockCollection: Collection, block: DataBlock) => {
return block.foreignKeyFields?.filter((foreignKeyField) => { return block.foreignKeyFields?.filter((foreignKeyField) => {
return filterBlockCollection.fields.some( return filterBlockCollection.fields.some((field) => {
(field) => field.type !== 'belongsTo' && field.foreignKey === foreignKeyField.name, 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( 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', field: string | ((target: FilterTarget['targets'][0], block: DataBlock) => string) = 'id',
operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq', operator: string | ((target: FilterTarget['targets'][0]) => string) = '$eq',
) => { ) => {
const currentBlock = dataBlocks.find((block) => block.uid === fieldSchema.parent['x-uid']);
dataBlocks.forEach((block) => { dataBlocks.forEach((block) => {
let key = field as string;
const target = targets.find((target) => target.uid === block.uid); const target = targets.find((target) => target.uid === block.uid);
if (!target) return; if (!target) return;
if (_.isFunction(value)) { if (_.isFunction(value)) {
value = value(target, block); value = value(target, block, getSourceKey(currentBlock, target.field));
} }
if (_.isFunction(field)) { if (_.isFunction(field)) {
field = field(target, block); key = field(target, block);
} }
if (_.isFunction(operator)) { if (_.isFunction(operator)) {
operator = operator(target); operator = operator(target);
@ -219,7 +225,7 @@ export const useFilterAPI = () => {
storedFilter[uid] = { storedFilter[uid] = {
$and: [ $and: [
{ {
[field]: { [key]: {
[operator]: value, [operator]: value,
}, },
}, },
@ -248,7 +254,7 @@ export const useFilterAPI = () => {
); );
}); });
}, },
[dataBlocks, targets, uid], [dataBlocks, targets, uid, fieldSchema],
); );
return { return {
@ -268,3 +274,8 @@ export const isInFilterFormBlock = (fieldSchema: Schema) => {
} }
return false; return false;
}; };
function getSourceKey(currentBlock: DataBlock, field: string) {
const associationField = currentBlock?.associatedFields?.find((item) => item.foreignKey === field);
return associationField?.sourceKey || field?.split?.('.')?.[1];
}

View File

@ -49,6 +49,8 @@ export interface CustomToken extends AliasToken {
marginBlock: number; marginBlock: number;
/** 区块的圆角 */ /** 区块的圆角 */
borderRadiusBlock: number; borderRadiusBlock: number;
siderWidth: number;
} }
export interface ThemeConfig extends _ThemeConfig { export interface ThemeConfig extends _ThemeConfig {

View File

@ -886,5 +886,8 @@
"Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?", "Are you sure you want to hide this tab?": "Sind Sie sicher, dass Sie diesen Tab ausblenden möchten?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen.", "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Nach dem Ausblenden wird dieser Tab nicht mehr in der Tableiste angezeigt. Um ihn wieder anzuzeigen, müssen Sie zur Routenverwaltungsseite gehen, um ihn einzustellen.",
"No pages yet, please configure first": "Noch keine Seiten, bitte zuerst konfigurieren", "No pages yet, please configure first": "Noch keine Seiten, bitte zuerst konfigurieren",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Klicken Sie auf das \"UI-Editor\"-Symbol in der oberen rechten Ecke, um den UI-Editor-Modus zu betreten" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Klicken Sie auf das \"UI-Editor\"-Symbol in der oberen rechten Ecke, um den UI-Editor-Modus zu betreten",
"Refresh data blocks": "Aktualisieren Sie die Datenblöcke",
"Select data blocks to refresh": "Wählen Sie die Datenblöcke aus, die aktualisiert werden sollen.",
"After successful submission, the selected data blocks will be automatically refreshed.": "Nach erfolgreicher Übermittlung werden die ausgewählten Datenblöcke automatisch aktualisiert."
} }

View File

@ -890,5 +890,9 @@
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode",
"Deprecated": "Deprecated", "Deprecated": "Deprecated",
"The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.", "The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version.",
"Full permissions": "Full permissions" "Full permissions": "Full permissions",
"Refresh data blocks": "Refresh data blocks",
"Select data blocks to refresh": "Select data blocks to refresh",
"After successful submission, the selected data blocks will be automatically refreshed.": "After successful submission, the selected data blocks will be automatically refreshed."
} }

View File

@ -807,5 +807,8 @@
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Haga clic en el icono \"Editor de UI\" en la esquina superior derecha para entrar en el modo de Editor de UI.", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Haga clic en el icono \"Editor de UI\" en la esquina superior derecha para entrar en el modo de Editor de UI.",
"Deprecated": "Obsoleto", "Deprecated": "Obsoleto",
"The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.", "The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión.",
"Full permissions": "Todos los derechos" "Full permissions": "Todos los derechos",
"Refresh data blocks": "Actualizar bloques de datos",
"Select data blocks to refresh": "Actualizar bloques de datos",
"After successful submission, the selected data blocks will be automatically refreshed.": "Después de enviar correctamente, los bloques de datos seleccionados se actualizarán automáticamente."
} }

View File

@ -827,5 +827,8 @@
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
"Deprecated": "Déprécié", "Deprecated": "Déprécié",
"The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.", "The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version.",
"Full permissions": "Tous les droits" "Full permissions": "Tous les droits",
"Refresh data blocks": "Actualiser les blocs de données",
"Select data blocks to refresh": "Actualiser les blocs de données",
"After successful submission, the selected data blocks will be automatically refreshed.": "Après une soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
} }

View File

@ -1081,6 +1081,12 @@
"If selected, the route will be displayed in the menu.": "Se selezionato, il percorso verrà visualizzato nel menu.", "If selected, the route will be displayed in the menu.": "Se selezionato, il percorso verrà visualizzato nel menu.",
"Are you sure you want to hide this tab?": "Sei sicuro di voler nascondere questa scheda?", "Are you sure you want to hide this tab?": "Sei sicuro di voler nascondere questa scheda?",
"After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Dopo averla nascosta, questa scheda non apparirà più nella barra delle schede. Per mostrarla di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo.", "After hiding, this tab will no longer appear in the tab bar. To show it again, you need to go to the route management page to set it.": "Dopo averla nascosta, questa scheda non apparirà più nella barra delle schede. Per mostrarla di nuovo, devi andare alla pagina di gestione dei percorsi per configurarlo.",
"Refresh data blocks": "Aggiorna blocchi di dati",
"Select data blocks to refresh": "Aggiorna blocchi di dati",
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés.",
"No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima", "No pages yet, please configure first": "Nessuna pagina ancora, si prega di configurare prima",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
"Refresh data blocks": "Aggiorna blocchi di dati",
"Select data blocks to refresh": "Aggiorna blocchi di dati",
"After successful submission, the selected data blocks will be automatically refreshed.": "Dopo una soumission réussie, les blocs de données sélectionnés seront automatiquement actualisés."
} }

View File

@ -1045,5 +1045,8 @@
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "ユーザーインターフェースエディターモードに入るには、右上隅の「UIエディタ」アイコンをクリックしてください", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "ユーザーインターフェースエディターモードに入るには、右上隅の「UIエディタ」アイコンをクリックしてください",
"Deprecated": "非推奨", "Deprecated": "非推奨",
"The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。", "The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。",
"Full permissions": "すべての権限" "Full permissions": "すべての権限",
"Refresh data blocks": "データブロックを更新",
"Select data blocks to refresh": "データブロックを選択して更新",
"After successful submission, the selected data blocks will be automatically refreshed.": "送信後、選択したデータブロックが自動的に更新されます。"
} }

View File

@ -918,5 +918,8 @@
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "사용자 인터페이스 편집기 모드에 들어가려면 오른쪽 상단의 \"UI 편집기\" 아이콘을 클릭하십시오", "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "사용자 인터페이스 편집기 모드에 들어가려면 오른쪽 상단의 \"UI 편집기\" 아이콘을 클릭하십시오",
"Deprecated": "사용 중단됨", "Deprecated": "사용 중단됨",
"The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.", "The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다.",
"Full permissions": "모든 권한" "Full permissions": "모든 권한",
"Refresh data blocks": "데이터 블록 새로 고침",
"Select data blocks to refresh": "데이터 블록을 선택하여 새로 고침",
"After successful submission, the selected data blocks will be automatically refreshed.": "전송 후, 선택한 데이터 블록이 자동으로 새로 고쳐집니다."
} }

View File

@ -1054,5 +1054,8 @@
"Font Sizepx": "Lettergroottepx", "Font Sizepx": "Lettergroottepx",
"Font Weight": "Letterdikte", "Font Weight": "Letterdikte",
"Font Style": "Letterstijl", "Font Style": "Letterstijl",
"Italic": "Cursief" "Italic": "Cursief",
"Refresh data blocks": "Vernieuw gegevensblokken",
"Select data blocks to refresh": "Selecteer gegevensblokken om te vernieuwen",
"After successful submission, the selected data blocks will be automatically refreshed.": "Na succesvolle indiening worden de geselecteerde gegevensblokken automatisch vernieuwd."
} }

View File

@ -783,6 +783,12 @@
"Deprecated": "Descontinuado", "Deprecated": "Descontinuado",
"The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.", "The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão.",
"Full permissions": "Todas as permissões", "Full permissions": "Todas as permissões",
"Refresh data blocks": "Atualizar blocos de dados",
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida.",
"No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro", "No pages yet, please configure first": "Ainda não há páginas, por favor configure primeiro",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Cliquez sur l'icône \"Éditeur d'interface utilisateur\" dans le coin supérieur droit pour entrer en mode Éditeur d'interface utilisateur",
"Refresh data blocks": "Atualizar blocos de dados",
"Select data blocks to refresh": "Selecionar blocos de dados para atualizar",
"After successful submission, the selected data blocks will be automatically refreshed.": "Após a atualização em massa bem sucedida."
} }

View File

@ -612,6 +612,12 @@
"Deprecated": "Устаревший", "Deprecated": "Устаревший",
"The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.", "The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии.",
"Full permissions": "Полные права", "Full permissions": "Полные права",
"Refresh data blocks": "Обновить блоки данных",
"Select data blocks to refresh": "Выберите блоки данных для обновления",
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены.",
"No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала", "No pages yet, please configure first": "Пока нет страниц, пожалуйста, настройте сначала",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Нажмите на значок \"Редактор пользовательского интерфейса\" в правом верхнем углу, чтобы войти в режим редактора пользовательского интерфейса",
"Refresh data blocks": "Обновить блоки данных",
"Select data blocks to refresh": "Выберите блоки данных для обновления",
"After successful submission, the selected data blocks will be automatically refreshed.": "После успешной отправки выбранные блоки данных будут автоматически обновлены."
} }

View File

@ -610,6 +610,12 @@
"Deprecated": "Kullanımdan kaldırıldı", "Deprecated": "Kullanımdan kaldırıldı",
"The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.", "The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır.",
"Full permissions": "Tüm izinler", "Full permissions": "Tüm izinler",
"Refresh data blocks": "Yenile veri blokları",
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir.",
"No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın", "No pages yet, please configure first": "Henüz sayfa yok, lütfen önce yapılandırın",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Kullanıcı arayüzü düzenleyici moduna girmek için sağ üst köşedeki \"Kullanıcı Arayüzü Düzenleyici\" simgesine tıklayın",
"Refresh data blocks": "Yenile veri blokları",
"Select data blocks to refresh": "Veri bloklarını yenilemek için seçin",
"After successful submission, the selected data blocks will be automatically refreshed.": "Başarılı bir şekilde gönderildikten sonra, seçilen veri blokları otomatik olarak yenilenecektir."
} }

View File

@ -826,6 +826,12 @@
"Deprecated": "Застаріло", "Deprecated": "Застаріло",
"The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.", "The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії.",
"Full permissions": "Повні права", "Full permissions": "Повні права",
"Refresh data blocks": "Оновити дані блоків",
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені.",
"No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте", "No pages yet, please configure first": "Ще немає сторінок, будь ласка, спочатку налаштуйте",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу." "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "Натисніть на значок \"Редактор користувацького інтерфейсу\" в правому верхньому куті, щоб увійти в режим редактора користувацького інтерфейсу.",
"Refresh data blocks": "Оновити дані блоків",
"Select data blocks to refresh": "Виберіть блоки даних для оновлення",
"After successful submission, the selected data blocks will be automatically refreshed.": "Після успішної подачі вибрані блоки даних будуть автоматично оновлені."
} }

View File

@ -167,6 +167,8 @@
"Year": "年", "Year": "年",
"QuarterYear": "季度", "QuarterYear": "季度",
"Select grouping field": "选择分组字段", "Select grouping field": "选择分组字段",
"Refresh data blocks": "刷新数据区块",
"Select data blocks to refresh": "选择要刷新的数据区块",
"Media": "多媒体", "Media": "多媒体",
"Markdown": "Markdown", "Markdown": "Markdown",
"Wysiwyg": "富文本", "Wysiwyg": "富文本",
@ -819,6 +821,7 @@
"File size should not exceed {{size}}.": "文件大小不能超过 {{size}}", "File size should not exceed {{size}}.": "文件大小不能超过 {{size}}",
"File size exceeds the limit": "文件大小超过限制", "File size exceeds the limit": "文件大小超过限制",
"File type is not allowed": "文件类型不允许", "File type is not allowed": "文件类型不允许",
"Uploading": "上传中",
"Incomplete uploading files need to be resolved": "未完成上传的文件需要处理", "Incomplete uploading files need to be resolved": "未完成上传的文件需要处理",
"Default title for each record": "用作数据的默认标题", "Default title for each record": "用作数据的默认标题",
"If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源", "If collection inherits, choose inherited collections as templates": "当前表有继承关系时,可选择继承链路上的表作为模板来源",
@ -1098,6 +1101,8 @@
"Italic": "斜体", "Italic": "斜体",
"Response record":"响应结果记录", "Response record":"响应结果记录",
"Colon":"冒号", "Colon":"冒号",
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。",
"No pages yet, please configure first": "暂无页面,请先配置", "No pages yet, please configure first": "暂无页面,请先配置",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "点击右上角的“界面配置”图标,进入界面配置模式",
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功后,会自动刷新这里选中的数据区块。"
} }

View File

@ -917,6 +917,12 @@
"Deprecated": "已棄用", "Deprecated": "已棄用",
"The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。", "The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。",
"Full permissions": "完全權限", "Full permissions": "完全權限",
"Refresh data blocks": "刷新數據區塊",
"Select data blocks to refresh": "選擇要刷新的數據區塊",
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。",
"No pages yet, please configure first": "尚未配置頁面,請先配置", "No pages yet, please configure first": "尚未配置頁面,請先配置",
"Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式" "Click the \"UI Editor\" icon in the upper right corner to enter the UI Editor mode": "點擊右上角的 \"介面設定\" 圖示進入介面設定模式",
"Refresh data blocks": "刷新數據區塊",
"Select data blocks to refresh": "選擇要刷新的數據區塊",
"After successful submission, the selected data blocks will be automatically refreshed.": "提交成功後,選中的數據區塊將自動刷新。"
} }

View File

@ -15,6 +15,8 @@ import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/actio
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingsEnableChildCollections } from '../../../schema-settings/SchemaSettings';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
import { useDataBlockProps } from '../../../data-source';
export const addNewActionSettings = new SchemaSettings({ export const addNewActionSettings = new SchemaSettings({
name: 'actionSettings:addNew', name: 'actionSettings:addNew',
@ -27,6 +29,16 @@ export const addNewActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'openMode', name: 'openMode',
Component: SchemaSettingOpenModeSchemaItems, Component: SchemaSettingOpenModeSchemaItems,

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { RecordPickerProvider, RecordPickerContext } from '../../../schema-component/antd/record-picker';
import { import {
SchemaComponentOptions, SchemaComponentOptions,
@ -41,9 +41,16 @@ const useTableSelectorProps = () => {
export const AssociateActionProvider = (props) => { export const AssociateActionProvider = (props) => {
const [selectedRows, setSelectedRows] = useState([]); const [selectedRows, setSelectedRows] = useState([]);
const collection = useCollection(); const collection = useCollection();
const { resource, service, block, __parent } = useBlockRequestContext(); const { resource, block, __parent } = useBlockRequestContext();
const actionCtx = useActionContext(); const actionCtx = useActionContext();
const { isMobile } = useOpenModeContext() || {}; const { isMobile } = useOpenModeContext() || {};
const [associationData, setAssociationData] = useState([]);
useEffect(() => {
resource?.list?.().then((res) => {
setAssociationData(res.data?.data || []);
});
}, [resource]);
const pickerProps = { const pickerProps = {
size: 'small', size: 'small',
onChange: props?.onChange, onChange: props?.onChange,
@ -73,8 +80,8 @@ export const AssociateActionProvider = (props) => {
}; };
const getFilter = () => { const getFilter = () => {
const targetKey = collection?.filterTargetKey || 'id'; const targetKey = collection?.filterTargetKey || 'id';
if (service.data?.data) { if (associationData) {
const list = service.data?.data.map((option) => option[targetKey]).filter(Boolean); const list = associationData.map((option) => option[targetKey]).filter(Boolean);
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {}; const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter; return filter;
} }

View File

@ -15,6 +15,7 @@ import {
SecondConFirm, SecondConFirm,
RefreshDataBlockRequest, RefreshDataBlockRequest,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const bulkDeleteActionSettings = new SchemaSettings({ export const bulkDeleteActionSettings = new SchemaSettings({
name: 'actionSettings:bulkDelete', name: 'actionSettings:bulkDelete',
@ -27,6 +28,16 @@ export const bulkDeleteActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConFirm', name: 'secondConFirm',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -32,11 +32,9 @@ export const disassociateActionSettings = new SchemaSettings({
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
}, },

View File

@ -14,7 +14,7 @@ import { useDesignable } from '../../..';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsModalItem } from '../../../schema-settings'; import { SchemaSettingsModalItem, SchemaSettingsLinkageRules } from '../../../schema-settings';
function ButtonEditor() { function ButtonEditor() {
const field = useField(); const field = useField();
@ -110,6 +110,17 @@ export const expendableActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'remove', name: 'remove',
sort: 100, sort: 100,

View File

@ -11,10 +11,9 @@ import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash'; import _ from 'lodash';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionRecord, useDesignable } from '../../../'; import { useDesignable } from '../../../';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { import {
SchemaSettingsLinkageRules, SchemaSettingsLinkageRules,
@ -22,6 +21,7 @@ import {
SchemaSettingAccessControl, SchemaSettingAccessControl,
} from '../../../schema-settings'; } from '../../../schema-settings';
import { useURLAndHTMLSchema } from './useURLAndHTMLSchema'; import { useURLAndHTMLSchema } from './useURLAndHTMLSchema';
import { useDataBlockProps } from '../../../data-source';
export const SchemaSettingsActionLinkItem: FC = () => { export const SchemaSettingsActionLinkItem: FC = () => {
const field = useField(); const field = useField();
@ -94,16 +94,10 @@ export const customizeLinkActionSettings = new SchemaSettings({
{ {
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useVisible() {
const record = useCollectionRecord();
return !_.isEmpty(record?.data);
},
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
}, },

View File

@ -10,7 +10,7 @@
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton, SecondConFirm } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
export const refreshActionSettings = new SchemaSettings({ export const refreshActionSettings = new SchemaSettings({
name: 'actionSettings:refresh', name: 'actionSettings:refresh',
items: [ items: [
@ -22,6 +22,17 @@ export const refreshActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConFirm', name: 'secondConFirm',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -29,6 +29,7 @@ import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider'; import { useParentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
import { useDataBlockProps } from '../../../data-source'; import { useDataBlockProps } from '../../../data-source';
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
const Tree = connect( const Tree = connect(
AntdTree, AntdTree,
@ -149,6 +150,16 @@ export const createSubmitActionSettings = new SchemaSettings({
return buttonEditorProps; return buttonEditorProps;
}, },
}, },
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{ {
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,

View File

@ -46,10 +46,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
collectionName: name, collectionName: name,
}; };
}, },
useVisible() {
const fieldSchema = useFieldSchema();
return !fieldSchema.parent['x-initializer'].includes('bulkEditForm');
},
}, },
{ {
name: 'secondConfirmation', name: 'secondConfirmation',

View File

@ -6,17 +6,12 @@
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { useFieldSchema } from '@formily/react';
import { useSchemaToolbar } from '../../../application'; import { useSchemaToolbar } from '../../../application';
import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../collection-manager';
import { useCollection } from '../../../data-source';
import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer'; import { ButtonEditor, RemoveButton } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings'; import { SchemaSettingsLinkageRules, SchemaSettingAccessControl } from '../../../schema-settings';
import { useOpenModeContext } from '../../popup/OpenModeProvider'; import { useOpenModeContext } from '../../popup/OpenModeProvider';
import { useCurrentPopupRecord } from '../../variable/variablesProvider/VariablePopupRecordProvider';
export const customizePopupActionSettings = new SchemaSettings({ export const customizePopupActionSettings = new SchemaSettings({
name: 'actionSettings:popup', name: 'actionSettings:popup',
@ -33,18 +28,11 @@ export const customizePopupActionSettings = new SchemaSettings({
name: 'linkageRules', name: 'linkageRules',
Component: SchemaSettingsLinkageRules, Component: SchemaSettingsLinkageRules,
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated();
const { linkageRulesProps } = useSchemaToolbar(); const { linkageRulesProps } = useSchemaToolbar();
return { return {
...linkageRulesProps, ...linkageRulesProps,
collectionName: name,
}; };
}, },
useVisible() {
const { collection } = useCurrentPopupRecord() || {};
const currentCollection = useCollection();
return !collection || collection?.name === currentCollection?.name;
},
}, },
{ {
name: 'openMode', name: 'openMode',

View File

@ -35,11 +35,16 @@ test.describe('linkage rules', () => {
// 条件singleLineText 字段的值包含 123 时 // 条件singleLineText 字段的值包含 123 时
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('select-filter-field').click();
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').click();
await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').fill('123');
await page.getByLabel('variable-button').first().click();
await page.getByText('Current form').last().click();
await page.getByText('Current form').last().click();
await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).locator('div').click();
// await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click();
await page.getByTestId('right-filter-field').getByRole('textbox').click();
await page.getByTestId('right-filter-field').getByRole('textbox').fill('123');
await page.getByRole('tabpanel').getByRole('textbox').last().fill('123');
// action禁用 longText 字段 // action禁用 longText 字段
await page.getByText('Add property').click(); await page.getByText('Add property').click();
await page.getByTestId('select-linkage-property-field').click(); await page.getByTestId('select-linkage-property-field').click();
@ -81,7 +86,7 @@ test.describe('linkage rules', () => {
// 修改第一组规则,使其条件中包含一个变量 -------------------------------------------------------------------------- // 修改第一组规则,使其条件中包含一个变量 --------------------------------------------------------------------------
// 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段 // 当 singleLineText 字段的值包含 longText 字段的值时,禁用 longText 字段
await openLinkageRules(); await openLinkageRules();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').last().click();
await expectSupportedVariables(page, [ await expectSupportedVariables(page, [
'Constant', 'Constant',
'Current user', 'Current user',
@ -136,8 +141,13 @@ test.describe('linkage rules', () => {
.getByText('Add condition', { exact: true }) .getByText('Add condition', { exact: true })
.last() .last()
.click(); .click();
await page.getByRole('button', { name: 'Select field' }).click(); // await page.getByRole('button', { name: 'Select field' }).click();
await page.getByRole('menuitemcheckbox', { name: 'number' }).click();
await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
await page.getByText('Current form').last().click();
await page.getByText('Current form').last().click();
await page.getByRole('menuitemcheckbox', { name: 'number' }).locator('div').click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').click(); await page.getByLabel('Linkage rules').getByRole('spinbutton').click();
await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123'); await page.getByLabel('Linkage rules').getByRole('spinbutton').fill('123');

View File

@ -22,7 +22,7 @@ test.describe('deprecated variables', () => {
await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible(); await expect(page.getByLabel('variable-tag').getByText('Current record / Nickname')).toBeVisible();
// 2. 但是变量列表中是禁用状态 // 2. 但是变量列表中是禁用状态
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } }); await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).hover({ position: { x: 40, y: 12 } });
await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible(); await expect(page.getByRole('tooltip', { name: 'This variable has been deprecated' })).toBeVisible();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass( await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toHaveClass(
@ -45,11 +45,11 @@ test.describe('deprecated variables', () => {
await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); await page.getByLabel('Linkage rules').getByText('Linkage rules').click();
// 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示 // 3. 当设置为其它变量后,再次打开,变量列表中的弃用变量不再显示
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1); await expect(page.getByRole('menuitemcheckbox', { name: 'Current form right' })).toHaveCount(1);
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname')).toBeVisible(); await expect(page.getByLabel('variable-tag').getByText('Current form / Nickname').last()).toBeVisible();
// 清空表达式 // 清空表达式
await page.getByLabel('textbox').clear(); await page.getByLabel('textbox').clear();
await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByRole('button', { name: 'OK', exact: true }).click();
@ -58,7 +58,7 @@ test.describe('deprecated variables', () => {
await page.getByLabel('block-item-CardItem-users-form').hover(); await page.getByLabel('block-item-CardItem-users-form').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:editForm-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.locator('button').filter({ hasText: /^x$/ }).click(); await page.locator('button').filter({ hasText: /^x$/ }).last().click();
await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden(); await expect(page.getByRole('menuitemcheckbox', { name: 'Current record right' })).toBeHidden();
// 使下拉菜单消失 // 使下拉菜单消失
await page.getByLabel('Linkage rules').getByText('Linkage rules').click(); await page.getByLabel('Linkage rules').getByText('Linkage rules').click();

View File

@ -86,7 +86,6 @@ test.describe('configure fields', () => {
await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.getByRole('menuitem', { name: 'manyToOne3' }).click();
await page.mouse.move(600, 0); await page.mouse.move(600, 0);
await page.reload(); await page.reload();
await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText(
`manyToOne1:${record.manyToOne1.id}`, `manyToOne1:${record.manyToOne1.id}`,
); );

View File

@ -42,6 +42,7 @@ test.describe('where grid card block can be added', () => {
await page.getByLabel('schema-initializer-Grid-').nth(1).hover(); await page.getByLabel('schema-initializer-Grid-').nth(1).hover();
await page.getByRole('menuitem', { name: 'Role name' }).click(); await page.getByRole('menuitem', { name: 'Role name' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.reload();
await expect(page.getByText('Root')).toBeVisible(); await expect(page.getByText('Root')).toBeVisible();
await expect(page.getByText('Admin')).toBeVisible(); await expect(page.getByText('Admin')).toBeVisible();
await expect(page.getByText('Member')).toBeVisible(); await expect(page.getByText('Member')).toBeVisible();

View File

@ -30,7 +30,6 @@ test('action linkage by row data', async ({ page, mockPage }) => {
// 添加其他你需要的样式属性 // 添加其他你需要的样式属性
}; };
}); });
expect(adminEditActionStyle.opacity).not.toBe('0.1'); expect(adminEditActionStyle.opacity).not.toBe('0.1');
expect(rootEditActionStyle.opacity).not.toBe('1'); expect(rootEditActionStyle.opacity).not.toBe('1');
}); });

View File

@ -309,6 +309,7 @@ test.describe('configure actions column', () => {
await page.getByText('Actions', { exact: true }).hover({ force: true }); await page.getByText('Actions', { exact: true }).hover({ force: true });
await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.mouse.move(500, 0);
// await page.getByText('Actions', { exact: true }).hover({ force: true }); // await page.getByText('Actions', { exact: true }).hover({ force: true });
// await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); // await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover();

View File

@ -316,7 +316,8 @@ test.describe('actions schema settings', () => {
// 添加一个条件ID 等于 1 // 添加一个条件ID 等于 1
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByTestId('select-filter-field').click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1'); await page.getByRole('spinbutton').fill('1');
@ -340,7 +341,8 @@ test.describe('actions schema settings', () => {
// 添加一个条件ID 等于 1 // 添加一个条件ID 等于 1
await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click(); await page.getByRole('tabpanel').getByText('Add condition', { exact: true }).last().click();
await page.getByRole('button', { name: 'Select field' }).click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').last().click();
await page.getByText('Current record').last().click();
await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click(); await page.getByRole('menuitemcheckbox', { name: 'ID', exact: true }).click();
await page.getByRole('spinbutton').click(); await page.getByRole('spinbutton').click();
await page.getByRole('spinbutton').fill('1'); await page.getByRole('spinbutton').fill('1');
@ -902,7 +904,6 @@ test.describe('actions schema settings', () => {
await page.getByRole('menuitem', { name: 'Submit' }).click(); await page.getByRole('menuitem', { name: 'Submit' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover(); await page.getByLabel('designer-schema-settings-CardItem-TableBlockDesigner-treeCollection').hover();
await page.getByRole('menuitem', { name: 'Tree table' }).click(); await page.getByRole('menuitem', { name: 'Tree table' }).click();
@ -928,6 +929,7 @@ test.describe('actions schema settings', () => {
await page.getByLabel('schema-initializer-Grid-form:').hover(); await page.getByLabel('schema-initializer-Grid-form:').hover();
await page.getByRole('menuitem', { name: 'Parent', exact: true }).click(); await page.getByRole('menuitem', { name: 'Parent', exact: true }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);
await page.reload();
await expect( await expect(
page page
.getByLabel('block-item-CollectionField-') .getByLabel('block-item-CollectionField-')

View File

@ -37,7 +37,7 @@ const enabledIndexColumn: SchemaSettingsItemType = {
const { dn } = useDesignable(); const { dn } = useDesignable();
return { return {
title: t('Enable index column'), title: t('Enable index column'),
checked: field.decoratorProps.enableSelectColumn !== false, checked: field.decoratorProps.enableIndexÏColumn !== false,
onChange: async (enableIndexÏColumn) => { onChange: async (enableIndexÏColumn) => {
field.decoratorProps = field.decoratorProps || {}; field.decoratorProps = field.decoratorProps || {};
field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn; field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;

View File

@ -94,7 +94,11 @@ export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
// @ts-ignore // @ts-ignore
field.dataSource = uiSchema.enum; field.dataSource = uiSchema.enum;
const originalProps = 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 || {}); field.componentProps = merge(field.componentProps || {}, originalProps, dynamicProps || {});
}, [uiSchemaOrigin]); }, [uiSchemaOrigin]);

View File

@ -27,7 +27,8 @@ test.describe('options of Select field in linkage rule', () => {
await page.getByRole('switch', { name: 'On Off' }).click(); await page.getByRole('switch', { name: 'On Off' }).click();
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();
await page.reload(); await page.reload();
await expect(page.getByRole('option', { name: 'option2' })).toBeVisible(); await page.getByLabel('block-item-CollectionField-').click();
await expect(page.getByRole('option', { name: 'option2' }).last()).toBeVisible();
await expect(page.getByRole('option', { name: 'option3' })).toBeVisible(); await expect(page.getByRole('option', { name: 'option3' })).toBeVisible();
}); });
}); });

View File

@ -57,6 +57,7 @@ test.describe('add blocks to the popup', () => {
await page.getByRole('menuitem', { name: 'Details right' }).hover(); await page.getByRole('menuitem', { name: 'Details right' }).hover();
await page.getByRole('menuitem', { name: 'Associated records' }).last().hover(); await page.getByRole('menuitem', { name: 'Associated records' }).last().hover();
await page.getByRole('menuitem', { name: 'Roles' }).click(); await page.getByRole('menuitem', { name: 'Roles' }).click();
await page.mouse.move(300, 0);
await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover(); await page.getByLabel('schema-initializer-Grid-details:configureFields-roles').hover();
await page.getByRole('menuitem', { name: 'Role UID' }).click(); await page.getByRole('menuitem', { name: 'Role UID' }).click();
await page.mouse.move(300, 0); await page.mouse.move(300, 0);

View File

@ -215,7 +215,7 @@ test.describe('where to open a popup and what can be added to it', () => {
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).click();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.getByRole('menuitem', { name: 'One to many' }).click();
@ -282,7 +282,7 @@ test.describe('where to open a popup and what can be added to it', () => {
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover(); await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
await page.getByRole('menuitem', { name: 'Table right' }).hover(); await page.getByRole('menuitem', { name: 'Table right' }).click();
await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1); await expect(page.getByRole('menuitem', { name: 'Associated records' })).toHaveCount(1);
await page.getByRole('menuitem', { name: 'Associated records' }).hover(); await page.getByRole('menuitem', { name: 'Associated records' }).hover();
await page.getByRole('menuitem', { name: 'One to many' }).click(); await page.getByRole('menuitem', { name: 'One to many' }).click();

View File

@ -71,7 +71,7 @@ test.describe('sub page', () => {
expect(page.url()).not.toContain('/popups/'); expect(page.url()).not.toContain('/popups/');
// 确认是否回到了主页面 // 确认是否回到了主页面
await page.getByText('Users单层子页面Configure').hover(); // await page.getByText('Users单层子页面Configure').hover();
await expect( await expect(
page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }), page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }),
).toBeVisible(); ).toBeVisible();

View File

@ -18,7 +18,7 @@ test.describe('variables', () => {
await page.getByLabel('action-Action.Link-View-view-').hover(); await page.getByLabel('action-Action.Link-View-view-').hover();
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover(); await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByLabel('variable-button').click(); await page.getByTestId('left-filter-field').getByLabel('variable-button').click();
// 2. 断言应该显示的变量 // 2. 断言应该显示的变量
['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach( ['Constant', 'Current user', 'Current role', 'API token', 'Date variables', 'Current record'].forEach(

View File

@ -22,14 +22,14 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前表单中应该包含 “Nickname” 字段 // 当前表单中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
// 当前对象中应该包含 “Role UID” 字段 // 当前对象中应该包含 “Role UID” 字段
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
await page.getByText('Current object').click(); await page.getByText('Current object').click();
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Role UID' }).click();
@ -43,12 +43,12 @@ test.describe('variable: Current Record', () => {
await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
await page.getByText('Add condition', { exact: true }).click(); await page.getByText('Add condition', { exact: true }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前记录中应该包含 “Nickname” 字段 // 当前记录中应该包含 “Nickname” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current record right' }).click();
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
await page.getByLabel('variable-button').click(); await page.getByLabel('variable-button').first().click();
// 当前对象中应该包含 “Role UID” 字段 // 当前对象中应该包含 “Role UID” 字段
await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click();

View File

@ -88,7 +88,7 @@ const dividerTheme = {
}, },
}; };
export const PinnedPluginList = React.memo(() => { export const PinnedPluginList = React.memo((props: { onClick?: () => void }) => {
const { allowAll, snippets } = useACLRoleContext(); const { allowAll, snippets } = useACLRoleContext();
const getSnippetsAllow = (aclKey) => { const getSnippetsAllow = (aclKey) => {
return allowAll || aclKey === '*' || snippets?.includes(aclKey); return allowAll || aclKey === '*' || snippets?.includes(aclKey);
@ -98,6 +98,7 @@ export const PinnedPluginList = React.memo(() => {
return ( return (
<div className={pinnedPluginListClassName}> <div className={pinnedPluginListClassName}>
<div onClick={props.onClick}>
{Object.keys(ctx.items) {Object.keys(ctx.items)
.sort((a, b) => ctx.items[a].order - ctx.items[b].order) .sort((a, b) => ctx.items[a].order - ctx.items[b].order)
.filter((key) => getSnippetsAllow(ctx.items[key].snippet)) .filter((key) => getSnippetsAllow(ctx.items[key].snippet))
@ -105,6 +106,7 @@ export const PinnedPluginList = React.memo(() => {
const Action = get(components, ctx.items[key].component); const Action = get(components, ctx.items[key].component);
return Action ? <Action key={key} /> : null; return Action ? <Action key={key} /> : null;
})} })}
</div>
<ConfigProvider theme={dividerTheme}> <ConfigProvider theme={dividerTheme}>
<Divider type="vertical" /> <Divider type="vertical" />
</ConfigProvider> </ConfigProvider>

View File

@ -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]);
});
});

View File

@ -12,6 +12,7 @@ import ProLayout, { RouteContext, RouteContextType } from '@ant-design/pro-layou
import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header'; import { HeaderViewProps } from '@ant-design/pro-layout/es/components/Header';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { theme as antdTheme, ConfigProvider, Popover, Result, Tooltip } from 'antd'; import { theme as antdTheme, ConfigProvider, Popover, Result, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import React, { createContext, FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -53,7 +54,6 @@ import { KeepAlive } from './KeepAlive';
import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema'; import { NocoBaseDesktopRoute, NocoBaseDesktopRouteType } from './convertRoutesToSchema';
import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings'; import { MenuSchemaToolbar, ResetThemeTokenAndKeepAlgorithm } from './menuItemSettings';
import { userCenterSettings } from './userCenterSettings'; import { userCenterSettings } from './userCenterSettings';
import { createStyles } from 'antd-style';
export { KeepAlive, NocoBaseDesktopRouteType }; export { KeepAlive, NocoBaseDesktopRouteType };
@ -457,9 +457,22 @@ const popoverStyle = css`
const MobileActions: FC = (props) => { const MobileActions: FC = (props) => {
const { token } = useToken(); const { token } = useToken();
const [open, setOpen] = useState(false);
// 点击时立即关闭 Popover避免影响用户操作
const handleContentClick = useCallback(() => {
setOpen(false);
}, []);
return ( return (
<Popover rootClassName={popoverStyle} content={<PinnedPluginList />} color={token.colorBgHeader}> <Popover
rootClassName={popoverStyle}
content={<PinnedPluginList onClick={handleContentClick} />}
color={token.colorBgHeader}
trigger="click"
open={open}
onOpenChange={setOpen}
>
<div style={{ padding: '0 16px', display: 'flex', alignItems: 'center', height: '100%', marginRight: -16 }}> <div style={{ padding: '0 16px', display: 'flex', alignItems: 'center', height: '100%', marginRight: -16 }}>
<EllipsisOutlined <EllipsisOutlined
style={{ style={{
@ -503,6 +516,8 @@ const subMenuItemRender = (item, dom) => {
}; };
const CollapsedButton: FC<{ collapsed: boolean }> = (props) => { const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
const { token } = useToken();
return ( return (
<RouteContext.Consumer> <RouteContext.Consumer>
{(context) => {(context) =>
@ -515,7 +530,7 @@ const CollapsedButton: FC<{ collapsed: boolean }> = (props) => {
// Fix the issue where the collapse/expand button is covered by subpages // Fix the issue where the collapse/expand button is covered by subpages
.ant-pro-sider-collapsed-button { .ant-pro-sider-collapsed-button {
top: 64px; top: 64px;
left: ${props.collapsed ? 52 : 188}px; left: ${props.collapsed ? 52 : (token.siderWidth || 200) - 12}px;
z-index: 200; z-index: 200;
transition: left 0.2s; transition: left 0.2s;
} }
@ -671,7 +686,7 @@ export const InternalAdminLayout = () => {
<DndContext onDragEnd={onDragEnd}> <DndContext onDragEnd={onDragEnd}>
<ProLayout <ProLayout
contentStyle={contentStyle} contentStyle={contentStyle}
siderWidth={200} siderWidth={token.siderWidth || 200}
className={resetStyle} className={resetStyle}
location={location} location={location}
route={route} route={route}
@ -710,27 +725,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 NavigateToDefaultPage: FC = (props) => {
const { allAccessRoutes } = useAllAccessDesktopRoutes(); const { allAccessRoutes } = useAllAccessDesktopRoutes();
const location = useLocationNoUpdate(); const location = useLocationNoUpdate();
const defaultPageUid = getDefaultPageUid(allAccessRoutes); const defaultPageUid = findFirstPageRoute(allAccessRoutes)?.schemaUid;
return ( return (
<> <>
@ -962,16 +961,17 @@ function findRouteById(id: string, treeArray: any[]) {
return null; return null;
} }
function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) { export function findFirstPageRoute(routes: NocoBaseDesktopRoute[]) {
if (!routes) return; if (!routes) return;
for (const route of routes) { for (const route of routes.filter((item) => !item.hideInMenu)) {
if (route.type === NocoBaseDesktopRouteType.page) { if (route.type === NocoBaseDesktopRouteType.page) {
return route; return route;
} }
if (route.children?.length) { if (route.type === NocoBaseDesktopRouteType.group && route.children?.length) {
return findFirstPageRoute(route.children); const result = findFirstPageRoute(route.children);
if (result) return result;
} }
} }
} }

View File

@ -9,14 +9,16 @@
import { ISchema, useField, useFieldSchema } from '@formily/react'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import { isValid, uid } from '@formily/shared'; import { isValid, uid } from '@formily/shared';
import { ModalProps } from 'antd'; import { ModalProps, Select } from 'antd';
import React, { useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..'; import { useCompile, useDesignable } from '../..';
import { isInitializersSame, useApp } from '../../../application'; import { isInitializersSame, useApp, usePlugin } from '../../../application';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings'; import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar'; import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { highlightBlock, startScrollEndTracking, stopScrollEndTracking, unhighlightBlock } from '../../../filter-provider/highlightBlock';
import { FlagProvider } from '../../../flag-provider'; import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings'; import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
@ -32,10 +34,10 @@ import {
SchemaSettingsSwitchItem, SchemaSettingsSwitchItem,
} from '../../../schema-settings/SchemaSettings'; } from '../../../schema-settings/SchemaSettings';
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
import { useLinkageAction } from './hooks'; import { useLinkageAction } from './hooks';
import { requestSettingsSchema } from './utils';
import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions'; import { useAfterSuccessOptions } from './hooks/useGetAfterSuccessVariablesOptions';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable'; import { requestSettingsSchema } from './utils';
const MenuGroup = (props) => { const MenuGroup = (props) => {
return props.children; return props.children;
@ -294,14 +296,92 @@ const useVariableProps = (environmentVariables) => {
}; };
}; };
const hideDialog = (dialogClassName: string) => {
const dialogMask = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-mask`);
const dialogWrap = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-wrap`);
if (dialogMask) {
dialogMask.style.opacity = '0';
dialogMask.style.transition = 'opacity 0.5s ease';
}
if (dialogWrap) {
dialogWrap.style.opacity = '0';
dialogWrap.style.transition = 'opacity 0.5s ease';
}
}
const showDialog = (dialogClassName: string) => {
const dialogMask = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-mask`);
const dialogWrap = document.querySelector<HTMLElement>(`.${dialogClassName} > .ant-modal-wrap`);
if (dialogMask) {
dialogMask.style.opacity = '1';
dialogMask.style.transition = 'opacity 0.5s ease';
}
if (dialogWrap) {
dialogWrap.style.opacity = '1';
dialogWrap.style.transition = 'opacity 0.5s ease';
}
}
export const BlocksSelector = (props) => {
const { getAllDataBlocks } = useAllDataBlocks();
const allDataBlocks = getAllDataBlocks();
const compile = useCompile();
const { t } = useTranslation();
// 转换 allDataBlocks 为 Select 选项
const options = useMemo(() => {
return allDataBlocks.map(block => {
// 防止列表中出现已关闭的弹窗中的区块
if (!block.dom?.isConnected) {
return null;
}
const title = `${compile(block.collection.title)} #${block.uid.slice(0, 4)}`;
return {
label: title,
value: block.uid,
onMouseEnter() {
block.highlightBlock();
hideDialog('dialog-after-successful-submission');
startScrollEndTracking(block.dom, () => {
highlightBlock(block.dom.cloneNode(true) as HTMLElement, block.dom.getBoundingClientRect());
});
},
onMouseLeave() {
block.unhighlightBlock();
showDialog('dialog-after-successful-submission');
stopScrollEndTracking(block.dom);
unhighlightBlock();
}
}
}).filter(Boolean);
}, [allDataBlocks, t]);
return (
<Select
value={props.value}
mode="multiple"
allowClear
placeholder={t('Select data blocks to refresh')}
options={options}
onChange={props.onChange}
/>
);
}
export function AfterSuccess() { export function AfterSuccess() {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { onSuccess } = fieldSchema?.['x-action-settings'] || {}; const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
const environmentVariables = useGlobalVariable('$env'); const environmentVariables = useGlobalVariable('$env');
const templatePlugin: any = usePlugin('@nocobase/plugin-block-template');
const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.();
return ( return (
<SchemaSettingsModalItem <SchemaSettingsModalItem
dialogRootClassName='dialog-after-successful-submission'
width={700}
title={t('After successful submission')} title={t('After successful submission')}
initialValues={ initialValues={
onSuccess onSuccess
@ -382,6 +462,18 @@ export function AfterSuccess() {
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
'x-use-component-props': () => useVariableProps(environmentVariables), 'x-use-component-props': () => 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 } as ISchema
} }

View File

@ -67,21 +67,24 @@ const ActionModalContent: FC<{ footerNodeName: string; field: any; schema: any }
); );
export function useDelayedVisible(visible: boolean, delay = 200) { export function useDelayedVisible(visible: boolean, delay = 200) {
const [ready, setReady] = useState(false); const [ready, setReady] = useState(delay === 0);
useEffect(() => { useEffect(() => {
if (ready) {
return;
}
if (visible) { if (visible) {
const timer = setTimeout(() => setReady(true), delay); const timer = setTimeout(() => setReady(true), delay);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} else { } else {
setReady(false); setReady(false);
} }
}, [visible]); }, [delay, ready, visible]);
return ready; return ready;
} }
export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer( export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = observer(
(props) => { (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 { visible, setVisible, openSize = 'middle', modalProps } = useActionContext();
const actualWidth = width ?? openSizeWidthMap.get(openSize); const actualWidth = width ?? openSizeWidthMap.get(openSize);
const schema = useFieldSchema(); const schema = useFieldSchema();
@ -102,7 +105,7 @@ export const InternalActionModal: React.FC<ActionDrawerProps<ModalProps>> = obse
} }
const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0); const zIndex = getZIndex('modal', _zIndex || parentZIndex, props.level || 0);
const ready = useDelayedVisible(visible, 200); // 200ms 与 Modal 动画时间一致 const ready = useDelayedVisible(visible, delay); // 200ms 与 Modal 动画时间一致
return ( return (
<ActionContextNoRerender> <ActionContextNoRerender>

View File

@ -49,9 +49,11 @@ import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils'; import { linkageAction, setInitialActionState } from './utils';
import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant';
import { BlockContext } from '../../../block-provider/BlockProvider';
// 这个要放到最下面,否则会导致前端单测失败 // 这个要放到最下面,否则会导致前端单测失败
import { useApp } from '../../../application'; import { useApp } from '../../../application';
import { useAllDataBlocks } from '../page/AllDataBlocksProvider';
const useA = () => { const useA = () => {
return { return {
@ -95,12 +97,16 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const { designable } = useDesignable(); const { designable } = useDesignable();
const tarComponent = useComponent(component) || component; const tarComponent = useComponent(component) || component;
const variables = useVariables(); 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 { visibleWithURL, setVisibleWithURL } = usePopupUtils();
const { setSubmitted } = useActionContext(); const { setSubmitted } = useActionContext();
const { getAriaLabel } = useGetAriaLabelOfAction(title); const { getAriaLabel } = useGetAriaLabelOfAction(title);
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const app = useApp(); const app = useApp();
const { getAllDataBlocks } = useAllDataBlocks();
useEffect(() => { useEffect(() => {
if (field.stateOfLinkageRules) { if (field.stateOfLinkageRules) {
setInitialActionState(field); setInitialActionState(field);
@ -117,6 +123,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
conditionType: v.conditionType,
}, },
app.jsonLogic, app.jsonLogic,
); );
@ -131,7 +138,31 @@ export const Action: ComposedAction = withDynamicSchemaProps(
[onMouseEnter], [onMouseEnter],
); );
const handleClick = useMemo(() => {
return ( 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 (
<BlockContext.Provider value={{ name: 'action' }}>
<InternalAction <InternalAction
containerRefKey={containerRefKey} containerRefKey={containerRefKey}
fieldSchema={fieldSchema} fieldSchema={fieldSchema}
@ -144,7 +175,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
className={className} className={className}
type={props.type} type={props.type}
Designer={Designer} Designer={Designer}
onClick={onClick} onClick={handleClick}
confirm={confirm} confirm={confirm}
confirmTitle={confirmTitle} confirmTitle={confirmTitle}
popover={popover} popover={popover}
@ -162,6 +193,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
actionCallback={actionCallback} actionCallback={actionCallback}
{...others} {...others}
/> />
</BlockContext.Provider>
); );
}), }),
{ displayName: 'Action' }, { displayName: 'Action' },
@ -322,11 +354,7 @@ const InternalAction: React.FC<InternalActionProps> = observer(function Com(prop
} }
if (addChild) { if (addChild) {
return wrapSSR( return wrapSSR(<TreeRecordProvider parent={recordData}>{result}</TreeRecordProvider>) as React.ReactElement;
<RecordProvider record={null} parent={parentRecordData}>
<TreeRecordProvider parent={recordData}>{result}</TreeRecordProvider>
</RecordProvider>,
) as React.ReactElement;
} }
return wrapSSR(result) as React.ReactElement; return wrapSSR(result) as React.ReactElement;

View File

@ -72,7 +72,7 @@ const InternalActionBar: FC = (props: any) => {
<Portal> <Portal>
<DndContext> <DndContext>
<div <div
style={{ display: 'flex', alignItems: 'center', gap: 8, ...style, marginTop: 0 }} style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 0, ...style }}
{...others} {...others}
className={cx(others.className, 'nb-action-bar')} className={cx(others.className, 'nb-action-bar')}
> >

View File

@ -12,9 +12,9 @@ import { useCollection_deprecated, useCollectionFilterOptions } from '../../../.
import { useCollectionRecordData } from '../../../../data-source'; import { useCollectionRecordData } from '../../../../data-source';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCompile } from '../../../'; import { useCompile } from '../../../';
import { useBlockContext } from '../../../../block-provider/BlockProvider';
import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks'; import { usePopupVariable } from '../../../../schema-settings/VariableInput/hooks';
import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks'; import { useCurrentRoleVariable } from '../../../../schema-settings/VariableInput/hooks';
import { useFormBlockContext } from '../../../../block-provider';
export const useAfterSuccessOptions = () => { export const useAfterSuccessOptions = () => {
const collection = useCollection_deprecated(); const collection = useCollection_deprecated();
@ -23,7 +23,7 @@ export const useAfterSuccessOptions = () => {
const userFieldOptions = useCollectionFilterOptions('users', 'main'); const userFieldOptions = useCollectionFilterOptions('users', 'main');
const compile = useCompile(); const compile = useCompile();
const recordData = useCollectionRecordData(); const recordData = useCollectionRecordData();
const { name: blockType } = useBlockContext() || {}; const { form } = useFormBlockContext();
const [fields, userFields] = useMemo(() => { const [fields, userFields] = useMemo(() => {
return [compile(fieldsOptions), compile(userFieldOptions)]; return [compile(fieldsOptions), compile(userFieldOptions)];
}, [fieldsOptions, userFieldOptions]); }, [fieldsOptions, userFieldOptions]);
@ -32,7 +32,7 @@ export const useAfterSuccessOptions = () => {
const record = useCollectionRecordData(); const record = useCollectionRecordData();
return useMemo(() => { return useMemo(() => {
return [ return [
(record || blockType === 'form') && { (record || form) && {
value: '$record', value: '$record',
label: t('Response record', { ns: 'client' }), label: t('Response record', { ns: 'client' }),
children: [...fields], children: [...fields],
@ -62,5 +62,5 @@ export const useAfterSuccessOptions = () => {
children: null, children: null,
}, },
].filter(Boolean); ].filter(Boolean);
}, [recordData, t, fields, blockType, userFields]); }, [recordData, t, fields, form, userFields]);
}; };

View File

@ -32,7 +32,7 @@ export const useGetAriaLabelOfAction = (title: string) => {
let { name: blockName } = useBlockContext() || {}; let { name: blockName } = useBlockContext() || {};
const actionTitle = title || compile(fieldSchema.title); const actionTitle = title || compile(fieldSchema.title);
collectionName = collectionName ? `-${collectionName}` : ''; collectionName = collectionName ? `-${collectionName}` : '';
blockName = blockName ? `-${blockName}` : ''; blockName = blockName && blockName !== 'action' ? `-${blockName}` : '';
action = action ? `-${action}` : ''; action = action ? `-${action}` : '';
recordName = recordName ? `-${recordName}` : ''; recordName = recordName ? `-${recordName}` : '';

View File

@ -92,6 +92,7 @@ export type ActionDrawerProps<T = DrawerProps> = T & {
footerNodeName?: string; footerNodeName?: string;
/** 当前弹窗嵌套的层级 */ /** 当前弹窗嵌套的层级 */
level?: number; level?: number;
delay?: number;
}; };
export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & { export type ComposedActionDrawer<T = DrawerProps> = React.FC<ActionDrawerProps<T>> & {

View File

@ -87,12 +87,14 @@ export const linkageAction = async (
condition, condition,
variables, variables,
localVariables, localVariables,
conditionType,
}: { }: {
operator; operator;
field; field;
condition; condition;
variables: VariablesContextType; variables: VariablesContextType;
localVariables: VariableOption[]; localVariables: VariableOption[];
conditionType: 'advanced' | 'basic';
}, },
jsonLogic: any, jsonLogic: any,
) => { ) => {
@ -101,7 +103,7 @@ export const linkageAction = async (
switch (operator) { switch (operator) {
case ActionType.Visible: case ActionType.Visible:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
displayResult.push(operator); displayResult.push(operator);
field.data = field.data || {}; field.data = field.data || {};
field.data.hidden = false; field.data.hidden = false;
@ -113,7 +115,7 @@ export const linkageAction = async (
field.display = last(displayResult); field.display = last(displayResult);
break; break;
case ActionType.Hidden: 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 = field.data || {};
field.data.hidden = true; field.data.hidden = true;
} else { } else {
@ -122,7 +124,7 @@ export const linkageAction = async (
} }
break; break;
case ActionType.Disabled: case ActionType.Disabled:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(true); disableResult.push(true);
} }
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
@ -133,7 +135,7 @@ export const linkageAction = async (
field.componentProps['disabled'] = last(disableResult); field.componentProps['disabled'] = last(disableResult);
break; break;
case ActionType.Active: case ActionType.Active:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables, conditionType }, jsonLogic)) {
disableResult.push(false); disableResult.push(false);
} else { } else {
disableResult.push(!!field.componentProps?.['disabled']); disableResult.push(!!field.componentProps?.['disabled']);

View File

@ -34,7 +34,7 @@ import useServiceOptions, { useAssociationFieldContext } from './hooks';
const removeIfKeyEmpty = (obj, filterTargetKey) => { const removeIfKeyEmpty = (obj, filterTargetKey) => {
if (!obj || typeof obj !== 'object' || !filterTargetKey || Array.isArray(obj)) return obj; 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) => { export const AssociationFieldAddNewer = (props) => {
@ -106,8 +106,13 @@ const InternalAssociationSelect = observer(
useEffect(() => { useEffect(() => {
const initValue = isVariable(field.value) ? undefined : field.value; const initValue = isVariable(field.value) ? undefined : field.value;
const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue; const value = Array.isArray(initValue) ? initValue.filter(Boolean) : initValue;
setInnerValue(value); const result = removeIfKeyEmpty(value, filterTargetKey);
}, [field.value]); setInnerValue(result);
if (!isEqual(field.value, result)) {
field.value = result;
}
}, [field.value, filterTargetKey]);
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {

View File

@ -27,7 +27,7 @@ export const useGetAriaLabelOfBlockItem = (name?: string) => {
let { name: blockName } = useBlockContext() || {}; let { name: blockName } = useBlockContext() || {};
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { name: collectionName, getField } = useCollection_deprecated(); 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); const title = compile(fieldSchema['title']) || compile(getField(fieldSchema.name)?.uiSchema?.title);

View File

@ -50,7 +50,7 @@ describe('CollectionSelect', () => {
> >
<div <div
aria-label="block-item-demo title" aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz" class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1rquknz"
role="button" role="button"
> >
<div <div
@ -191,7 +191,7 @@ describe('CollectionSelect', () => {
> >
<div <div
aria-label="block-item-demo title" aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz" class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1rquknz"
role="button" role="button"
> >
<div <div

View File

@ -252,8 +252,9 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) {
const value = Array.isArray(props.value) ? props.value[0] : props.value; const value = Array.isArray(props.value) ? props.value[0] : props.value;
const compile = useCompile(); const compile = useCompile();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const targetPicker = value ? inferPickerType(value, picker) : picker; const initPicker = value ? inferPickerType(value, picker) : picker;
const targetDateFormat = getPickerFormat(targetPicker) || format; const [targetPicker, setTargetPicker] = useState(initPicker);
const targetDateFormat = getPickerFormat(initPicker) || format;
const newProps = { const newProps = {
utc, utc,
inputReadOnly: isMobileMedia, inputReadOnly: isMobileMedia,
@ -271,13 +272,6 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) {
}; };
const field: any = useField(); const field: any = useField();
const [stateProps, setStateProps] = useState(newProps); const [stateProps, setStateProps] = useState(newProps);
useEffect(() => {
newProps.picker = targetPicker;
const dateTimeFormat = getDateTimeFormat(targetPicker, targetDateFormat, showTime, timeFormat);
newProps.format = dateTimeFormat;
setStateProps(newProps);
}, [targetPicker]);
return ( return (
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%' }}>
<Select <Select
@ -307,6 +301,7 @@ DatePicker.FilterWithPicker = function FilterWithPicker(props: any) {
}, },
])} ])}
onChange={(value) => { onChange={(value) => {
setTargetPicker(value);
const format = getPickerFormat(value); const format = getPickerFormat(value);
const dateTimeFormat = getDateTimeFormat(value, format, showTime, timeFormat); const dateTimeFormat = getDateTimeFormat(value, format, showTime, timeFormat);
field.setComponentProps({ field.setComponentProps({

View File

@ -87,7 +87,7 @@ export const FormItem: any = withDynamicSchemaProps(
}} }}
/> />
) : ( ) : (
t(field.description, { ns: NAMESPACE_UI_SCHEMA }) field.description
); );
} }
}, [field.description]); }, [field.description]);
@ -112,9 +112,6 @@ export const FormItem: any = withDynamicSchemaProps(
? '100% !important' ? '100% !important'
: null}; : null};
} }
.ant-formily-item-control {
padding: ${showTitle === false ? '5px' : '0px'};
}
`, `,
)} )}
> >

View File

@ -101,6 +101,10 @@ const useLazyLoadDisplayAssociationFieldsOfForm = () => {
field.value = null; field.value = null;
} else { } else {
field.value = result; field.value = result;
field.componentProps = {
...field.componentProps,
readOnlySubmit: true,
}; // 让它不参与提交
} }
}); });
}) })

View File

@ -20,6 +20,7 @@ import { useAttach, useComponent } from '../..';
import { useApp } from '../../../application'; import { useApp } from '../../../application';
import { getCardItemSchema } from '../../../block-provider'; import { getCardItemSchema } from '../../../block-provider';
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
import { useDataBlockProps } from '../../../data-source';
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider'; import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
@ -150,12 +151,15 @@ const WithForm = (props: WithFormProps) => {
const linkageRules: any[] = const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
useEffect(() => { useEffect(() => {
const id = uid(); const id = uid();
form.addEffects(id, () => { form.addEffects(id, () => {
onFormInputChange(() => { onFormInputChange(() => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}); });
@ -166,7 +170,7 @@ const WithForm = (props: WithFormProps) => {
return () => { return () => {
form.removeEffects(id); form.removeEffects(id);
}; };
}, [form, props.disabled, setFormValueChanged]); }, [form, props.disabled, setFormValueChanged, confirmBeforeClose]);
useEffect(() => { useEffect(() => {
if (loading) { if (loading) {
@ -219,17 +223,19 @@ const WithForm = (props: WithFormProps) => {
const WithoutForm = (props) => { const WithoutForm = (props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext(); const { setFormValueChanged } = useActionContext();
// 关闭弹窗之前,如果有未保存的数据,是否要二次确认
const { confirmBeforeClose = true } = useDataBlockProps() || ({} as any);
const form = useMemo( const form = useMemo(
() => () =>
createForm({ createForm({
disabled: props.disabled, disabled: props.disabled,
effects() { effects() {
onFormInputChange((form) => { onFormInputChange((form) => {
setFormValueChanged?.(true); setFormValueChanged?.(confirmBeforeClose);
}); });
}, },
}), }),
[], [confirmBeforeClose],
); );
return fieldSchema['x-decorator'] === 'FormV2' ? ( return fieldSchema['x-decorator'] === 'FormV2' ? (
<FormDecorator form={form} {...props} /> <FormDecorator form={form} {...props} />

View File

@ -66,4 +66,5 @@ export * from './unix-timestamp';
export * from './upload'; export * from './upload';
export * from './variable'; export * from './variable';
export * from './form-drawer'; export * from './form-drawer';
export * from './linkageFilter';
import './index.less'; import './index.less';

Some files were not shown because too many files have changed in this diff Show More