Merge branch '2.0' into fields-flow-modal

This commit is contained in:
katherinehhh 2025-06-19 11:03:54 +08:00
commit 3590b0219f
224 changed files with 2193 additions and 705 deletions

View File

@ -36,7 +36,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: ./docker/nocobase
file: ./docker/nocobase/Dockerfile-cn
file: ./docker/nocobase/Dockerfile-full
build-args: |
CNA_VERSION=${{ inputs.tag_name }}
platforms: linux/amd64,linux/arm64

View File

@ -216,7 +216,7 @@ jobs:
uses: docker/build-push-action@v3
with:
context: ./docker/nocobase
file: ./docker/nocobase/Dockerfile-cn
file: ./docker/nocobase/Dockerfile-full
build-args: |
CNA_VERSION=${{ steps.get-info.outputs.defaultTag }}
platforms: linux/amd64,linux/arm64

View File

@ -5,6 +5,122 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.7.15](https://github.com/nocobase/nocobase/compare/v1.7.14...v1.7.15) - 2025-06-18
### 🐛 Bug Fixes
- **[client]**
- Use independent variable scope for each field ([#7012](https://github.com/nocobase/nocobase/pull/7012)) by @mytharcher
- Assign field values: Unable to clear data for relation fields ([#7086](https://github.com/nocobase/nocobase/pull/7086)) by @zhangzhonghe
- Table column text alignment function is not working ([#7094](https://github.com/nocobase/nocobase/pull/7094)) by @zhangzhonghe
- **[Workflow]** Fix incorrectly executed checking on big integer number ([#7091](https://github.com/nocobase/nocobase/pull/7091)) by @mytharcher
- **[File manager]** Fix attachments field can not be updated in approval process ([#7093](https://github.com/nocobase/nocobase/pull/7093)) by @mytharcher
- **[Workflow: Approval]** Use comparison instead of implicit logic to avoid type issues by @mytharcher
## [v1.7.14](https://github.com/nocobase/nocobase/compare/v1.7.13...v1.7.14) - 2025-06-17
### 🚀 Improvements
- **[client]** Auto-hide grid card block action bar when empty ([#7069](https://github.com/nocobase/nocobase/pull/7069)) by @zhangzhonghe
- **[Verification]** Remove verifier options from the response of the `verifiers:listByUser` API ([#7090](https://github.com/nocobase/nocobase/pull/7090)) by @2013xile
### 🐛 Bug Fixes
- **[database]** support association updates in updateOrCreate and firstOrCreate ([#7088](https://github.com/nocobase/nocobase/pull/7088)) by @chenos
- **[client]**
- URL query parameter variables not working in public form field default value ([#7084](https://github.com/nocobase/nocobase/pull/7084)) by @katherinehhh
- style condition on subtable column fields not applied correctly ([#7083](https://github.com/nocobase/nocobase/pull/7083)) by @katherinehhh
- Filtering through relationship collection fields in filter forms is invalid ([#7070](https://github.com/nocobase/nocobase/pull/7070)) by @zhangzhonghe
- **[Collection field: Many to many (array)]** Updating a many to many (array) field throws an error when the `updatedBy` field is present ([#7089](https://github.com/nocobase/nocobase/pull/7089)) by @2013xile
- **[Public forms]** Public forms: Fix unauthorized access issue on form submission ([#7085](https://github.com/nocobase/nocobase/pull/7085)) by @zhangzhonghe
## [v1.7.13](https://github.com/nocobase/nocobase/compare/v1.7.12...v1.7.13) - 2025-06-17
### 🚀 Improvements
- **[client]** Logo container width adapts to content type (fixed 168px for images, auto width for text) ([#7075](https://github.com/nocobase/nocobase/pull/7075)) by @Cyx649312038
- **[Workflow: Approval]** Add extra field option for re-assignees list by @mytharcher
### 🐛 Bug Fixes
- **[client]**
- required validation message in subtable persists when switching page ([#7080](https://github.com/nocobase/nocobase/pull/7080)) by @katherinehhh
- decimal point lost after switching amount component from mask to inputNumer ([#7077](https://github.com/nocobase/nocobase/pull/7077)) by @katherinehhh
- incorrect Markdown (Vditor) rendering in subtable ([#7074](https://github.com/nocobase/nocobase/pull/7074)) by @katherinehhh
- **[Collection field: Sequence]** Fix string based bigint sequence calculation ([#7079](https://github.com/nocobase/nocobase/pull/7079)) by @mytharcher
- **[Backup manager]** unknow command error when restoring MySQL backups on windows platform by @gchust
## [v1.7.12](https://github.com/nocobase/nocobase/compare/v1.7.11...v1.7.12) - 2025-06-16
### 🚀 Improvements
- **[client]** add "empty" and "not empty" options to checkbox field linkage rules ([#7073](https://github.com/nocobase/nocobase/pull/7073)) by @katherinehhh
### 🐛 Bug Fixes
- **[client]** After creating the reverse relation field, the option "Create reverse relation field in the target data table" in the association field settings was not checked. ([#6914](https://github.com/nocobase/nocobase/pull/6914)) by @aaaaaajie
- **[Data source manager]** Scope changes now take effect immediately for all related roles. ([#7065](https://github.com/nocobase/nocobase/pull/7065)) by @aaaaaajie
- **[Access control]** Fixed an issue where the app blocked entry when no default role existed ([#7059](https://github.com/nocobase/nocobase/pull/7059)) by @aaaaaajie
- **[Workflow: Custom action event]** Fix variable of redirect url not parsed by @mytharcher
## [v1.7.11](https://github.com/nocobase/nocobase/compare/v1.7.10...v1.7.11) - 2025-06-15
### 🎉 New Features
- **[Text copy]** Support one-click copying of text field content ([#6954](https://github.com/nocobase/nocobase/pull/6954)) by @zhangzhonghe
### 🐛 Bug Fixes
- **[client]**
- association field selector does not clear selected data after submission ([#7067](https://github.com/nocobase/nocobase/pull/7067)) by @katherinehhh
- Fix upload size hint ([#7057](https://github.com/nocobase/nocobase/pull/7057)) by @mytharcher
- **[server]** Cannot read properties of undefined (reading 'setMaaintainingMessage') ([#7064](https://github.com/nocobase/nocobase/pull/7064)) by @chenos
- **[Workflow: Loop node]** Fix loop branch runs when condition not satisfied ([#7063](https://github.com/nocobase/nocobase/pull/7063)) by @mytharcher
- **[Workflow: Approval]**
- Fix todo stats not updated when execution canceled by @mytharcher
- Fix trigger variable when filter by type by @mytharcher
## [v1.7.10](https://github.com/nocobase/nocobase/compare/v1.7.9...v1.7.10) - 2025-06-12
### 🐛 Bug Fixes
- **[client]**
- Fix the issue where linkage rules cause infinite loop ([#7050](https://github.com/nocobase/nocobase/pull/7050)) by @zhangzhonghe
- Fix: use optional chaining to safely reject requests in APIClient when handler may be undefined ([#7054](https://github.com/nocobase/nocobase/pull/7054)) by @sheldon66
- auto-closing issue when configuring fields in the secondary popup form ([#7052](https://github.com/nocobase/nocobase/pull/7052)) by @katherinehhh
- **[Data visualization]** incorrect display of between date field in chart filter ([#7051](https://github.com/nocobase/nocobase/pull/7051)) by @katherinehhh
- **[API documentation]** non-NocoBase official plugins fail to display API documentation ([#7045](https://github.com/nocobase/nocobase/pull/7045)) by @chenzhizdt
- **[Action: Import records]** Fixed xlsx import to restrict textarea fields from accepting non-string formatted data ([#7049](https://github.com/nocobase/nocobase/pull/7049)) by @aaaaaajie
## [v1.7.9](https://github.com/nocobase/nocobase/compare/v1.7.8...v1.7.9) - 2025-06-11
### 🐛 Bug Fixes

View File

@ -5,6 +5,122 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
## [v1.7.15](https://github.com/nocobase/nocobase/compare/v1.7.14...v1.7.15) - 2025-06-18
### 🐛 修复
- **[client]**
- 对每个字段使用独立的变量范围 ([#7012](https://github.com/nocobase/nocobase/pull/7012)) by @mytharcher
- 字段赋值:关系字段无法被清空数据 ([#7086](https://github.com/nocobase/nocobase/pull/7086)) by @zhangzhonghe
- 表格列的文本对齐功能无效 ([#7094](https://github.com/nocobase/nocobase/pull/7094)) by @zhangzhonghe
- **[工作流]** 修复已执行数在大整型数时检查错误的问题 ([#7091](https://github.com/nocobase/nocobase/pull/7091)) by @mytharcher
- **[文件管理器]** 修复审批处理中附件字段无法被更新的问题 ([#7093](https://github.com/nocobase/nocobase/pull/7093)) by @mytharcher
- **[工作流:审批]** 使用比较代替隐式逻辑以避免类型问题 by @mytharcher
## [v1.7.14](https://github.com/nocobase/nocobase/compare/v1.7.13...v1.7.14) - 2025-06-17
### 🚀 优化
- **[client]** 网格卡片区块操作栏为空时自动隐藏 ([#7069](https://github.com/nocobase/nocobase/pull/7069)) by @zhangzhonghe
- **[验证]** 移除 `verifiers:listByUser` 接口中响应的认证器配置信息 ([#7090](https://github.com/nocobase/nocobase/pull/7090)) by @2013xile
### 🐛 修复
- **[database]** 修复 updateOrCreate 和 firstOrCreate 不支持关系更新的问题 ([#7088](https://github.com/nocobase/nocobase/pull/7088)) by @chenos
- **[client]**
- 修复公开表单字段默认值中 URL 查询参数变量无效的问题 ([#7084](https://github.com/nocobase/nocobase/pull/7084)) by @katherinehhh
- 修复 子表格列字段 style 条件判断无效的问题 ([#7083](https://github.com/nocobase/nocobase/pull/7083)) by @katherinehhh
- 筛选表单中,通过关系表字段筛选无效 ([#7070](https://github.com/nocobase/nocobase/pull/7070)) by @zhangzhonghe
- **[数据表字段:多对多 (数组)]** 存在 `updatedBy` 字段的时,更新多对多(数组)字段报错 ([#7089](https://github.com/nocobase/nocobase/pull/7089)) by @2013xile
- **[公开表单]** 公开表单:修复提交表单时报无权限的问题 ([#7085](https://github.com/nocobase/nocobase/pull/7085)) by @zhangzhonghe
## [v1.7.13](https://github.com/nocobase/nocobase/compare/v1.7.12...v1.7.13) - 2025-06-17
### 🚀 优化
- **[client]** Logo 容器宽度根据内容类型自适应(图片固定 168px文本自动宽度 ([#7075](https://github.com/nocobase/nocobase/pull/7075)) by @Cyx649312038
- **[工作流:审批]** 为转签、加签的人员选择列表增加额外字段显示的配置项 by @mytharcher
### 🐛 修复
- **[client]**
- 修复子表格字段切换页面后必填提示不消失的问题 ([#7080](https://github.com/nocobase/nocobase/pull/7080)) by @katherinehhh
- 修复金额字段组件从掩码改为数字后小数点丢失的问题 ([#7077](https://github.com/nocobase/nocobase/pull/7077)) by @katherinehhh
- 修复子表格中 MarkdownVditor字段组件渲染不正确的问题 ([#7074](https://github.com/nocobase/nocobase/pull/7074)) by @katherinehhh
- **[数据表字段:自动编码]** 修复基于字符串的大整数序列计算 ([#7079](https://github.com/nocobase/nocobase/pull/7079)) by @mytharcher
- **[备份管理器]** windows 平台下,还原 MySQL 应用时提示无法识别的命令错误 by @gchust
## [v1.7.12](https://github.com/nocobase/nocobase/compare/v1.7.11...v1.7.12) - 2025-06-16
### 🚀 优化
- **[client]** checkbox 字段联动条件判断支持 "为空”和“不为空” ([#7073](https://github.com/nocobase/nocobase/pull/7073)) by @katherinehhh
### 🐛 修复
- **[client]** 创建反向关系字段后,编辑关系字段设置项“在目标数据表里创建反向关系字段”未勾选 ([#6914](https://github.com/nocobase/nocobase/pull/6914)) by @aaaaaajie
- **[数据源管理]** 修改权限的数据范围后,相关角色同步生效 ([#7065](https://github.com/nocobase/nocobase/pull/7065)) by @aaaaaajie
- **[权限控制]** 修复了在没有默认角色时无法进入应用的问题 ([#7059](https://github.com/nocobase/nocobase/pull/7059)) by @aaaaaajie
- **[工作流:自定义操作事件]** 修复操作成功后配置中的重定向链接变量未解析的问题 by @mytharcher
## [v1.7.11](https://github.com/nocobase/nocobase/compare/v1.7.10...v1.7.11) - 2025-06-15
### 🎉 新特性
- **[文本复制]** 支持一键复制文本字段内容 ([#6954](https://github.com/nocobase/nocobase/pull/6954)) by @zhangzhonghe
### 🐛 修复
- **[client]**
- 关系字段数据选择器提交后未清空选中数据 ([#7067](https://github.com/nocobase/nocobase/pull/7067)) by @katherinehhh
- 修复上传组件的大小提示文字 ([#7057](https://github.com/nocobase/nocobase/pull/7057)) by @mytharcher
- **[server]** Cannot read properties of undefined (reading 'setMaaintainingMessage') ([#7064](https://github.com/nocobase/nocobase/pull/7064)) by @chenos
- **[工作流:循环节点]** 修复循环分支在条件未满足时仍然执行的问题 ([#7063](https://github.com/nocobase/nocobase/pull/7063)) by @mytharcher
- **[工作流:审批]**
- 修复待办统计在执行计划取消后未更新的问题 by @mytharcher
- 修复触发器变量中按类型过滤的缺陷 by @mytharcher
## [v1.7.10](https://github.com/nocobase/nocobase/compare/v1.7.9...v1.7.10) - 2025-06-12
### 🐛 修复
- **[client]**
- 修复联动规则卡死的问题 ([#7050](https://github.com/nocobase/nocobase/pull/7050)) by @zhangzhonghe
- 修复:在 APIClient 中添加可选链以避免 handler 未定义时报错 ([#7054](https://github.com/nocobase/nocobase/pull/7054)) by @sheldon66
- 修复二级弹窗配置表单字段时自动关闭弹窗的问题 ([#7052](https://github.com/nocobase/nocobase/pull/7052)) by @katherinehhh
- **[数据可视化]** 修复图表区块中筛选表单的日期字段设置为“介于”时组件未正确显示的问题 ([#7051](https://github.com/nocobase/nocobase/pull/7051)) by @katherinehhh
- **[API 文档]** 非 NocoBase 官方插件无法展示API文档 ([#7045](https://github.com/nocobase/nocobase/pull/7045)) by @chenzhizdt
- **[操作:导入记录]** 导入 xlsx 禁止多行文本字段插入非字符串格式数据 ([#7049](https://github.com/nocobase/nocobase/pull/7049)) by @aaaaaajie
## [v1.7.9](https://github.com/nocobase/nocobase/compare/v1.7.8...v1.7.9) - 2025-06-11
### 🐛 修复

View File

@ -1,19 +0,0 @@
#!/bin/sh
set -e
nginx
echo 'nginx started';
cd /app/nocobase && yarn nocobase db:auth --retry=30
cd /app/nocobase && yarn nocobase install -s
cd /app/nocobase && yarn nocobase upgrade -S
cd /app/nocobase && yarn start
# Run command with node if the first argument contains a "-" or is not a system command. The last
# part inside the "{}" is a workaround for the following bug in ash/dash:
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then
set -- node "$@"
fi
exec "$@"

View File

@ -1,43 +0,0 @@
log_format apm '"$time_local" client=$remote_addr '
'method=$request_method request="$request" '
'request_length=$request_length '
'status=$status bytes_sent=$bytes_sent '
'body_bytes_sent=$body_bytes_sent '
'referer=$http_referer '
'user_agent="$http_user_agent" '
'upstream_addr=$upstream_addr '
'upstream_status=$upstream_status '
'request_time=$request_time '
'upstream_response_time=$upstream_response_time '
'upstream_connect_time=$upstream_connect_time '
'upstream_header_time=$upstream_header_time';
server {
listen 80;
server_name _;
root /app/nocobase/packages/app/client/dist;
index index.html;
client_max_body_size 0;
access_log /var/log/nginx/nocobase.log apm;
location /storage/uploads/ {
alias /app/nocobase/storage/uploads/;
autoindex off;
}
location / {
root /app/nocobase/packages/app/client/dist;
try_files $uri $uri/ /index.html;
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:13000/api/;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}

View File

@ -1,5 +1,5 @@
{
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"],

View File

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

View File

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

View File

@ -9,4 +9,7 @@
import { proxyToRepository } from './proxy-to-repository';
export const firstOrCreate = proxyToRepository(['values', 'filterKeys'], 'firstOrCreate');
export const firstOrCreate = proxyToRepository(
['values', 'filterKeys', 'whitelist', 'blacklist', 'updateAssociationValues', 'targetCollection'],
'firstOrCreate',
);

View File

@ -9,4 +9,7 @@
import { proxyToRepository } from './proxy-to-repository';
export const updateOrCreate = proxyToRepository(['values', 'filterKeys'], 'updateOrCreate');
export const updateOrCreate = proxyToRepository(
['values', 'filterKeys', 'whitelist', 'blacklist', 'updateAssociationValues', 'targetCollection'],
'updateOrCreate',
);

View File

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

View File

@ -1,16 +1,16 @@
{
"name": "@nocobase/auth",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/actions": "1.8.0-alpha.5",
"@nocobase/cache": "1.8.0-alpha.5",
"@nocobase/database": "1.8.0-alpha.5",
"@nocobase/resourcer": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/actions": "1.8.0-alpha.8",
"@nocobase/cache": "1.8.0-alpha.8",
"@nocobase/database": "1.8.0-alpha.8",
"@nocobase/resourcer": "1.8.0-alpha.8",
"@nocobase/utils": "1.8.0-alpha.8",
"@types/jsonwebtoken": "^9.0.9",
"jsonwebtoken": "^9.0.2"
},

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/build",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "Library build tool based on rollup.",
"main": "lib/index.js",
"types": "./lib/index.d.ts",

View File

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

View File

@ -117,4 +117,19 @@ describe('cache', () => {
expect(val2).toBe(obj);
expect(await cache.get('key')).toMatchObject(obj);
});
it('redis cache wrap null throw error', async () => {
if (!process.env.CACHE_REDIS_URL) {
return;
}
const cacheManager = new CacheManager({
stores: {
redis: {
url: process.env.CACHE_REDIS_URL,
},
},
});
const c = await cacheManager.createCache({ name: 'test', store: 'redis' });
expect(async () => c.wrap('test', async () => null)).rejects.toThrowError('"null" is not a cacheable value');
});
});

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/cli",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js"
},
"dependencies": {
"@nocobase/app": "1.8.0-alpha.5",
"@nocobase/app": "1.8.0-alpha.8",
"@nocobase/license-kit": "^0.2.3",
"@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20",
@ -27,7 +27,7 @@
"tsx": "^4.19.0"
},
"devDependencies": {
"@nocobase/devtools": "1.8.0-alpha.5"
"@nocobase/devtools": "1.8.0-alpha.8"
},
"repository": {
"type": "git",

View File

@ -115,7 +115,7 @@ exports.postCheck = async (opts) => {
const port = opts.port || process.env.APP_PORT;
const result = await exports.isPortReachable(port);
if (result) {
console.error(chalk.red(`post already in use ${port}`));
console.error(chalk.red(`Port ${port} already in use`));
process.exit(1);
}
};

View File

@ -1,6 +1,6 @@
import * as icons from '@ant-design/icons';
import { Plugin } from '@nocobase/client';
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { defineAction, defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button } from 'antd';
import React from 'react';
import { createApp } from './createApp';
@ -40,10 +40,7 @@ const myEventFlow = defineFlow({
MyModel.registerFlow(myEventFlow);
class PluginDemo extends Plugin {
async load() {
this.flowEngine.registerModels({ MyModel });
this.flowEngine.registerAction({
const myConfirm = defineAction({
name: 'confirm',
uiSchema: {
title: {
@ -73,7 +70,12 @@ class PluginDemo extends Plugin {
return ctx.exit();
}
},
});
});
class PluginDemo extends Plugin {
async load() {
this.flowEngine.registerModels({ MyModel });
this.flowEngine.registerAction(myConfirm);
const model = this.flowEngine.createModel({
use: 'MyModel',
});

View File

@ -6,7 +6,7 @@ import React from 'react';
// 实现一个本地存储的模型仓库,负责模型的持久化
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
// 从本地存储加载模型数据
async load(uid: string) {
async findOne({ uid }) {
const data = localStorage.getItem(`flow-model:${uid}`);
if (!data) return null;
return JSON.parse(data);

View File

@ -1,6 +1,6 @@
# FlowAction
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作Action的核心对象。每个操作Action封装一段可执行的业务逻辑,可在多个流步骤中复用支持参数配置、UI 配置和类型推断。
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作Action的核心对象。每个操作封装一段可执行的业务逻辑可在多个流步骤中复用支持参数配置、UI 配置和类型推断。
---
@ -10,48 +10,50 @@
interface ActionDefinition {
name: string; // 操作唯一标识,必须唯一
title?: string; // 操作显示名称(可选)
uiSchema?: Record<string, ISchema>; // (可选)用于参数配置界面渲染
uiSchema?: Record<string, ISchema>; // (可选)参数配置界面渲染
defaultParams?: Record<string, any>; // (可选)默认参数
paramsRequired?: boolean; // (可选)是否需要参数配置为true时添加模型前会打开配置对话框
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏该步骤
paramsRequired?: boolean; // (可选)是否需要参数配置
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏
handler: (ctx: FlowContext, params: any) => Promise<any> | any; // 操作执行逻辑
}
```
---
## 定义操作的方式
## 使用说明
### 1. 使用 defineAction 工具函数
### 1. 定义 Action
推荐方式,结构清晰、类型推断友好:
#### 方式一:使用 defineAction 工具函数(推荐)
结构清晰,类型推断友好:
```ts
const myAction = defineAction({
name: 'actionName',
name: 'myAction',
title: '操作显示名称',
uiSchema: {},
defaultParams: {},
paramsRequired: true, // 添加模型前强制打开配置对话框
hideInSettings: false, // 在设置菜单中显示
paramsRequired: true,
hideInSettings: false,
async handler(ctx, params) {
// 操作逻辑
},
});
```
### 2. 实现 ActionDefinition 接口
#### 方式二:实现 ActionDefinition 接口
适合需要扩展属性或方法的场景:
复杂场景时可以通过定义 Action 类来处理更复杂的操作
```ts
class MyAction implements ActionDefinition {
name = 'actionName';
name = 'myAction';
title = '操作显示名称';
uiSchema = {};
defaultParams = {};
paramsRequired = true; // 添加模型前强制打开配置对话框
hideInSettings = false; // 在设置菜单中显示
paramsRequired = true;
hideInSettings = false;
async handler(ctx, params) {
// 操作逻辑
}
@ -60,59 +62,115 @@ class MyAction implements ActionDefinition {
---
## 注册操作
注册后可在流步骤中通过 `use` 字段复用:
### 2. 注册到 FlowEngine 里
```ts
flowEngine.registerAction({
name: 'actionName',
title: '操作显示名称',
uiSchema: {},
defaultParams: {},
paramsRequired: true, // 添加模型前强制打开配置对话框
hideInSettings: false, // 在设置菜单中显示
handler(ctx, params) {
// 操作逻辑
},
});
flowEngine.registerAction(myAction); // 注册 defineAction 返回的对象
flowEngine.registerAction(new MyAction()); // 注册类实例
```
---
## 在流中复用操作
### 3. 在流中使用
在流步骤定义中通过 `use` 字段引用已注册的操作:
```ts
steps: {
step1: {
use: 'actionName', // 复用已注册的操作
use: 'myAction', // 复用已注册的操作
defaultParams: {},
paramsRequired: true, // 可以在步骤级别覆盖操作的paramsRequired设置
hideInSettings: false, // 可以在步骤级别覆盖操作的hideInSettings设置
paramsRequired: true, // 可覆盖操作的 paramsRequired
hideInSettings: false, // 可覆盖操作的 hideInSettings
},
}
```
---
## 配置选项说明
## 参数配置详解
### name
- **类型**: `string`
- **说明**: 操作唯一标识,必须全局唯一。建议使用有业务含义的英文名,便于维护和复用。
### title
- **类型**: `string`
- **说明**: 操作的显示名称,通常用于界面展示。支持多语言配置。
### defaultParams
- **类型**: `Record<string, any>``(ctx) => Record<string, any>`
- **说明**: 操作参数的默认值。支持静态对象或函数(可根据 context 动态生成)。
- **作用**: 作为 handler 的 params 默认值。
**静态用法:**
```ts
{
defaultParams: { key1: 'val1' },
async handler(ctx, params) {
console.log(params.key1); // val1
},
}
```
**动态用法:**
```ts
{
defaultParams(ctx) {
return { key1: 'val1' }
},
async handler(ctx, params) {
console.log(params.key1); // val1
},
}
```
### handler
- **类型**: `(ctx: FlowContext, params: any) => Promise<any> | any`
- **说明**: 操作的核心执行逻辑。支持异步和同步函数。`ctx` 提供当前流上下文,`params` 为参数对象。
### uiSchema
- **类型**: `Omit<FormilySchema, 'default'>`
- **说明**: 用于参数的可视化配置表单。推荐与 defaultParams 配合使用,提升用户体验。
- **注意**: uiSchema 不支持 default 参数,避免与 defaultParams 重复。
### paramsRequired
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 当设置为 `true` 时,在添加该步骤模型前会强制打开参数配置对话框,确保用户配置必要的参数。适用于需要用户必须配置参数才能正常工作的操作。
- **说明**: 为 `true` 时,添加步骤前会强制打开参数配置对话框,确保用户配置必要参数。适用于参数必填的场景
### hideInSettings
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 当设置为 `true` 时,该步骤将在设置菜单中隐藏,用户无法通过 Settings 界面直接添加该步骤。适用于初始化配置场景。
- **说明**: 为 `true` 时,该步骤在设置菜单中隐藏,用户无法通过 Settings 界面直接添加。适用于初始化配置或内部步骤。
---
## 最佳实践
- 推荐优先使用 `defineAction` 工具函数定义操作,结构更清晰,类型推断更友好。
- `name` 字段建议采用有业务含义的英文名,避免重名。
- `uiSchema``defaultParams` 配合使用,提升参数配置体验。
- 对于需要用户强制配置参数的操作,设置 `paramsRequired: true`
- 内部或自动化步骤可设置 `hideInSettings: true`,避免用户误操作。
---
## 常见问题与注意事项
- **Q: defaultParams 和 uiSchema 有什么区别?**
> defaultParams 用于设置参数默认值uiSchema 用于渲染参数配置表单。两者配合使用,互不冲突。
- **Q: uiSchema 为什么不支持 default**
> 1. 为避免与 defaultParams 重复uiSchema 仅用于表单结构描述,不处理默认值;
> 2. 使用 defaultParams 处理可以有更好的 ts 类型提示;
> 3. uiSchema 的结构可能较为复杂,解析 uiSchema 来提取 default 值非常繁琐且容易出错,因此不建议在 uiSchema 中处理 default
---
@ -121,3 +179,4 @@ steps: {
- **FlowAction** 让流步骤逻辑高度复用,便于维护和扩展。
- 支持多种定义方式,适应不同复杂度的业务场景。
- 可通过 `uiSchema``defaultParams` 配置参数界面和默认值,提升易用性。
- 合理使用 `paramsRequired``hideInSettings`,提升操作安全性和灵活性。

View File

@ -4,7 +4,7 @@
## 主要方法
- **load(uid: string): Promise<FlowModel \| null>**
- **findOne(query: Query): Promise<FlowModel \| null>**
根据唯一标识符 uid 从远程加载模型数据。
- **save(model: FlowModel): Promise<any>**
@ -19,7 +19,8 @@
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
constructor(private app: Application) {}
async load(uid: string) {
async findOne(query) {
const { uid, parentId } = query;
// 实现:根据 uid 获取模型
return null;
}

View File

@ -4,7 +4,7 @@ import React from 'react';
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
constructor(private app: Application) {}
async load(uid: string) {
async findOne({ uid, parentId }) {
// implement fetching a model by id
return null;
}

View File

@ -6,7 +6,9 @@ import { Button, Tabs } from 'antd';
import _ from 'lodash';
import React from 'react';
class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }>> {
class FlowModelRepository
implements IFlowModelRepository<FlowModel<{ parent: never; subModels: { tabs: TabFlowModel[] } }>>
{
get models() {
const models = new Map();
for (let i = 0; i < localStorage.length; i++) {
@ -22,8 +24,12 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
return models;
}
async findOne(query) {
return this.load(query.uid);
}
// 从本地存储加载模型数据
async load(uid: string) {
async load({ uid }) {
const data = localStorage.getItem(`flow-model:${uid}`);
if (!data) return null;
const json: FlowModel = JSON.parse(data);
@ -57,7 +63,10 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
localStorage.setItem(`flow-model:${subModel.uid}`, JSON.stringify(subModel.serialize()));
});
} else if (model.subModels[subModelKey] instanceof FlowModel) {
localStorage.setItem(`flow-model:${model.subModels[subModelKey].uid}`, JSON.stringify(model.subModels[subModelKey].serialize()));
localStorage.setItem(
`flow-model:${model.subModels[subModelKey].uid}`,
JSON.stringify(model.subModels[subModelKey].serialize()),
);
}
}
return data;
@ -72,8 +81,7 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
class TabFlowModel extends FlowModel {}
class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }> {
class HelloFlowModel extends FlowModel<{ parent: never; subModels: { tabs: TabFlowModel[] } }> {
addTab(tab: any) {
// 使用新的 addSubModel API 添加子模型
const model = this.addSubModel('tabs', tab);
@ -88,7 +96,7 @@ class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlo
items={this.subModels.tabs?.map((tab) => ({
key: tab.getProps().key,
label: tab.getProps().label,
children: tab.render()
children: tab.render(),
}))}
tabBarExtraContent={
<Button
@ -98,7 +106,7 @@ class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlo
use: 'TabFlowModel',
uid: tabId,
props: { key: tabId, label: `Tab - ${tabId}` },
})
});
}}
>
Add Tab
@ -134,7 +142,7 @@ class PluginHelloModel extends Plugin {
props: { key: 'tab-2', label: 'Tab 2' },
},
],
}
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}

View File

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

View File

@ -72,7 +72,7 @@ export class APIClient extends APIClientSDK {
api.notification = this.notification;
const handlers = [];
for (const handler of this.axios.interceptors.response['handlers']) {
if (handler.rejected['_name'] === 'handleNotificationError') {
if (handler?.rejected?.['_name'] === 'handleNotificationError') {
handlers.push({
...handler,
rejected: api.handleNotificationError.bind(api),

View File

@ -19,9 +19,10 @@ import { useDetailsProps } from '../modules/blocks/data-blocks/details-single/ho
import { FormItemSchemaToolbar } from '../modules/blocks/data-blocks/form/FormItemSchemaToolbar';
import { useCreateFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps';
import { useCreateFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps';
import { useDataFormItemProps } from '../modules/blocks/data-blocks/form/hooks/useDataFormItemProps';
import { useEditFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps';
import { useEditFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockProps';
import { useDataFormItemProps } from '../modules/blocks/data-blocks/form/hooks/useDataFormItemProps';
import { useGridCardActionBarProps } from '../modules/blocks/data-blocks/grid-card/hooks/useGridCardActionBarProps';
import {
useGridCardBlockDecoratorProps,
useGridCardBlockItemProps,
@ -97,6 +98,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useGridCardBlockProps,
useFormItemProps,
useDataFormItemProps,
useGridCardActionBarProps,
}}
>
{props.children}
@ -161,6 +163,7 @@ export class BlockSchemaComponentPlugin extends Plugin {
useGridCardBlockItemProps,
useFormItemProps,
useDataFormItemProps,
useGridCardActionBarProps,
});
}
}

View File

@ -63,7 +63,7 @@ interface Props {
expandFlag?: boolean;
dragSortBy?: string;
association?: string;
enableIndexÏColumn?: boolean;
enableIndexColumn?: boolean;
}
const InternalTableBlockProvider = (props: Props) => {
@ -77,7 +77,7 @@ const InternalTableBlockProvider = (props: Props) => {
fieldNames,
collection,
association,
enableIndexÏColumn,
enableIndexColumn,
} = props;
const field: any = useField();
const { resource, service } = useBlockRequestContext();
@ -136,7 +136,7 @@ const InternalTableBlockProvider = (props: Props) => {
setExpandFlag: setExpandFlagValue,
heightProps,
association,
enableIndexÏColumn,
enableIndexColumn,
}),
[
allIncludesChildren,
@ -153,7 +153,7 @@ const InternalTableBlockProvider = (props: Props) => {
setExpandFlagValue,
showIndex,
association,
enableIndexÏColumn,
enableIndexColumn,
],
);

View File

@ -197,9 +197,7 @@ export function useCollectValuesToSubmit() {
if (isVariable(value)) {
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue !== null && parsedValue !== undefined) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value !== '') {
assignedValues[key] = value;
}
@ -385,9 +383,7 @@ export const useAssociationCreateActionProps = () => {
if (isVariable(value)) {
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value !== '') {
assignedValues[key] = value;
}
@ -658,9 +654,7 @@ export const useCustomizeUpdateActionProps = () => {
if (isVariable(value)) {
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value !== '') {
assignedValues[key] = value;
}
@ -771,9 +765,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
if (isVariable(value)) {
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value !== '') {
assignedValues[key] = value;
}
@ -999,9 +991,7 @@ export const useUpdateActionProps = () => {
if (isVariable(value)) {
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
if (parsedValue) {
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
}
} else if (value !== '') {
assignedValues[key] = value;
}

View File

@ -193,6 +193,8 @@ export const EditFieldAction = (props) => {
defaultValues.reverseField = interfaceConf?.default?.reverseField;
set(defaultValues.reverseField, 'name', `f_${uid()}`);
set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title);
} else {
defaultValues.autoCreateReverseField = true;
}
const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer);
setSchema(schema);

View File

@ -24,7 +24,7 @@ export class CheckboxFieldInterface extends CollectionFieldInterface {
'x-component': 'Checkbox',
},
};
availableTypes = ['boolean', 'integer', 'bigInt'];
availableTypes = ['boolean', 'integer', 'bigInt', 'bit'];
hasDefaultValue = true;
properties = {
...defaultProps,

View File

@ -242,6 +242,8 @@ export const boolean = [
},
},
},
{ label: "{{ t('is empty') }}", value: '$empty', noValue: true },
{ label: "{{ t('is not empty') }}", value: '$notEmpty', noValue: true },
];
export const tableoid = [

View File

@ -159,7 +159,7 @@ export class InheritanceCollectionMixin extends Collection {
const targetField = filterFields.find((k) => {
return k.name === v.name;
});
return targetField.collectionName !== this.name;
return targetField?.collectionName !== this.name;
});
return this.parentCollectionFields[parentCollectionName];
}

View File

@ -58,9 +58,13 @@ export const fieldComponentSettingsItem: SchemaSettingsItemType = {
value: fieldSchema['x-component-props']?.['component'] || options[0]?.value,
onChange(component) {
const componentOptions = options.find((item) => item.value === component);
const baseProps = componentOptions?.useProps?.() || {};
const componentProps = {
component,
...(componentOptions?.useProps?.() || {}),
...baseProps,
...(component === collectionField['uiSchema']['x-component']
? collectionField['uiSchema']['x-component-props']
: {}),
};
_.set(fieldSchema, 'x-component-props', componentProps);
field.componentProps = componentProps;

View File

@ -118,11 +118,17 @@ export const transformToFilter = (
) {
return true;
}
if (value?.type) {
const collectionField = getCollectionJoinField(`${collectionName}.${path}`);
if (
['datetime', 'datetimeNoTz', 'date', 'unixTimestamp', 'createdAt', 'updatedAt'].includes(
collectionField?.interface,
)
) {
return true;
}
const collectionField = getCollectionJoinField(`${collectionName}.${path}`);
if (collectionField?.target) {
if (Array.isArray(value)) {
return true;

View File

@ -26,6 +26,26 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
return models;
}
async findOne(query) {
const { uid, parentId } = query;
if (uid) {
return this.load(uid);
} else if (parentId) {
return this.loadByParentId(parentId);
}
return null;
}
async loadByParentId(parentId: string) {
for (const model of this.models.values()) {
if (model.parentId == parentId) {
console.log('Loading model by parentId:', parentId, model);
return this.load(model.uid);
}
}
return null;
}
// 从本地存储加载模型数据
async load(uid: string) {
const data = localStorage.getItem(`flow-model:${uid}`);
@ -43,10 +63,12 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
json.subModels[model.subKey].push(subModel);
} else if (model.subType === 'object') {
const subModel = await this.load(model.uid);
if (subModel) {
json.subModels[model.subKey] = subModel;
}
}
}
}
console.log('Loading model:', uid, JSON.stringify(json, null, 2));
return json;
}

View File

@ -10,7 +10,7 @@
import { FlowModelRenderer, useFlowEngine, useFlowModel } from '@nocobase/flow-engine';
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import React from 'react';
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
function InternalFlowPage({ uid, sharedContext }) {
@ -20,15 +20,16 @@ function InternalFlowPage({ uid, sharedContext }) {
export const FlowPage = () => {
const params = useParams();
return <FlowPageComponent uid={params.name} sharedContext={{}} />;
return <FlowPageComponent uid={params.name} />;
};
export const FlowPageComponent = ({ uid, sharedContext }) => {
export const FlowPageComponent = (props) => {
const { uid, parentId, sharedContext } = props;
const flowEngine = useFlowEngine();
const { loading } = useRequest(
() => {
return flowEngine.loadOrCreateModel({
uid: uid,
const { loading, data } = useRequest(
async () => {
const options = {
uid,
use: 'PageFlowModel',
subModels: {
tabs: [
@ -42,14 +43,22 @@ export const FlowPageComponent = ({ uid, sharedContext }) => {
},
],
},
});
};
if (!uid && parentId) {
options['async'] = true;
options['parentId'] = parentId;
options['subKey'] = 'page';
options['subType'] = 'object';
}
const data = await flowEngine.loadOrCreateModel(options);
return data;
},
{
refreshDeps: [uid],
refreshDeps: [uid || parentId],
},
);
if (loading) {
if (loading || !data?.uid) {
return <Spin />;
}
return <InternalFlowPage uid={uid} sharedContext={sharedContext} />;
return <InternalFlowPage uid={data.uid} sharedContext={sharedContext} />;
};

View File

@ -0,0 +1,46 @@
/**
* 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 type { ButtonType } from 'antd/es/button';
import React from 'react';
import { FlowPageComponent } from '../FlowPage';
import { ActionModel } from './ActionModel';
export class AddNewActionModel extends ActionModel {
title = 'Add new';
}
AddNewActionModel.registerFlow({
key: 'event1',
on: {
eventName: 'click',
},
steps: {
step1: {
handler(ctx, params) {
// eslint-disable-next-line prefer-const
let currentDrawer: any;
function DrawerContent() {
return (
<div>
<FlowPageComponent parentId={ctx.model.uid} sharedContext={{ ...ctx.extra, currentDrawer }} />
</div>
);
}
currentDrawer = ctx.globals.drawer.open({
title: '命令式 Drawer',
width: 800,
content: <DrawerContent />,
});
},
},
},
});

View File

@ -65,7 +65,7 @@ export class FormModel extends BlockFlowModel {
/>
<FormButtonGroup>
{this.mapSubModels('actions', (action) => (
<FlowModelRenderer model={action} showFlowSettings />
<FlowModelRenderer model={action} showFlowSettings extraContext={{ currentModel: this }} />
))}
<AddActionButton model={this} subModelBaseClass="ActionModel" />
</FormButtonGroup>
@ -107,20 +107,22 @@ FormModel.registerFlow({
},
async handler(ctx, params) {
ctx.model.form = ctx.extra.form || createForm();
if (ctx.model.collection) {
return;
}
ctx.model.collection = ctx.globals.dataSourceManager.getCollection(params.dataSourceKey, params.collectionName);
if (!ctx.model.collection) {
ctx.model.collection = ctx.globals.dataSourceManager.getCollection(
params.dataSourceKey,
params.collectionName,
);
const resource = new SingleRecordResource();
resource.setDataSourceKey(params.dataSourceKey);
resource.setResourceName(params.collectionName);
resource.setAPIClient(ctx.globals.api);
ctx.model.resource = resource;
console.log('FormModel flow context', ctx.extra);
if (ctx.extra.currentRecord) {
resource.setFilterByTk(ctx.extra.currentRecord.id);
await resource.refresh();
ctx.model.form.setInitialValues(resource.getData());
}
console.log('FormModel flow context', ctx.shared, ctx.model.getSharedContext());
if (ctx.shared.currentRecord) {
ctx.model.resource.setFilterByTk(ctx.shared.currentRecord.id);
await ctx.model.resource.refresh();
ctx.model.form.setInitialValues(ctx.model.resource.getData());
}
},
},

View File

@ -25,11 +25,16 @@ SubmitActionModel.registerFlow({
steps: {
step1: {
async handler(ctx, params) {
await ctx.model.parent.form.submit();
const values = ctx.model.parent.form.values;
await ctx.model.parent.resource.save(values);
if (ctx.model.parent.dialog) {
ctx.model.parent.dialog.close();
if (ctx.extra.currentModel) {
await ctx.extra.currentModel.form.submit();
const values = ctx.extra.currentModel.form.values;
await ctx.extra.currentModel.resource.save(values);
}
if (ctx.shared.currentDrawer) {
ctx.shared.currentDrawer.destroy();
}
if (ctx.shared.currentResource) {
ctx.shared.currentResource.refresh();
}
},
},

View File

@ -115,11 +115,11 @@ TableColumnModel.define({
sort: 0,
});
const Columns = observer<any>(({ record, model }) => {
const Columns = observer<any>(({ record, model, index }) => {
return (
<Space>
{model.mapSubModels('actions', (action: ActionModel) => {
const fork = action.createFork({}, `${record.id}`);
const fork = action.createFork({}, `${index}`);
return (
<FlowModelRenderer
showFlowSettings
@ -183,7 +183,7 @@ export class TableActionsColumnModel extends TableColumnModel {
}
render() {
return (value, record, index) => <Columns record={record} model={this} />;
return (value, record, index) => <Columns record={record} model={this} index={index} />;
}
}

View File

@ -10,6 +10,7 @@
import { SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import {
AddActionButton,
AddActionModel,
AddFieldButton,
Collection,
@ -108,10 +109,20 @@ export class TableModel extends BlockFlowModel<S> {
extraContext={{ currentModel: this, currentResource: this.resource }}
/>
))}
<AddActionModel
<AddActionButton model={this} subModelBaseClass="ActionModel">
<Button icon={<SettingOutlined />}>Configure actions</Button>
</AddActionButton>
{/* <AddActionModel
model={this}
subModelKey={'actions'}
items={() => [
{
key: 'addnew',
label: 'Add new',
createModelOptions: {
use: 'AddNewActionModel',
},
},
{
key: 'delete',
label: 'Delete',
@ -121,10 +132,8 @@ export class TableModel extends BlockFlowModel<S> {
},
]}
>
<Button type="primary" icon={<SettingOutlined />}>
Configure actions
</Button>
</AddActionModel>
<Button icon={<SettingOutlined />}>Configure actions</Button>
</AddActionModel> */}
</Space>
<Table
className={css`

View File

@ -7,7 +7,6 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { uid } from '@formily/shared';
import type { ButtonType } from 'antd/es/button';
import React from 'react';
import { FlowPageComponent } from '../FlowPage';
@ -26,14 +25,21 @@ ViewActionModel.registerFlow({
steps: {
step1: {
handler(ctx, params) {
ctx.globals.drawer.open({
// eslint-disable-next-line prefer-const
let currentDrawer: any;
function DrawerContent() {
return (
<div>
<FlowPageComponent parentId={ctx.model.uid} sharedContext={{ ...ctx.extra, currentDrawer }} />
</div>
);
}
currentDrawer = ctx.globals.drawer.open({
title: '命令式 Drawer',
width: 800,
content: (
<div>
<FlowPageComponent uid={`${ctx.model.uid}-drawer`} sharedContext={ctx.extra} />
</div>
),
content: <DrawerContent />,
});
},
},

View File

@ -8,9 +8,10 @@
*/
export * from './ActionModel';
export * from './BulkDeleteActionModel';
export * from './AddNewActionModel';
export * from './BlockFlowModel';
export * from './BlockGridFlowModel';
export * from './BulkDeleteActionModel';
export * from './CalendarBlockFlowModel';
export * from './DeleteActionModel';
export * from './FormFieldModel';

View File

@ -12,7 +12,8 @@ export const useEvaluatedExpression = (expression: string) => {
useEffect(() => {
const run = async () => {
if (!expression) {
if (expression == null || expression === '') {
setParsedValue(undefined);
return;
}

View File

@ -27,12 +27,8 @@ describe('createGridCardBlockSchema', () => {
"actionBar": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": "var(--nb-spacing)",
},
},
"x-initializer": "gridCard:configureActions",
"x-use-component-props": "useGridCardActionBarProps",
},
"list": {
"properties": {
@ -103,12 +99,8 @@ describe('createGridCardBlockSchema', () => {
"actionBar": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": "var(--nb-spacing)",
},
},
"x-initializer": "gridCard:configureActions",
"x-use-component-props": "useGridCardActionBarProps",
},
"list": {
"properties": {

View File

@ -49,11 +49,7 @@ export const createGridCardBlockUISchema = (options: {
type: 'void',
'x-initializer': 'gridCard:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-use-component-props': 'useGridCardActionBarProps',
},
list: {
type: 'array',

View File

@ -0,0 +1,26 @@
/**
* 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 { useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useDesignable } from '../../../../../schema-component';
export const useGridCardActionBarProps = () => {
const fieldSchema = useFieldSchema();
const { designable } = useDesignable();
return {
style: {
marginBottom: 'var(--nb-spacing)',
},
// In non-configuration mode, when there are no buttons, ActionBar doesn't need to be displayed
hidden: !designable && _.isEmpty(fieldSchema.properties),
};
};

View File

@ -52,6 +52,7 @@ test.describe('where table block can be added', () => {
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'Associated records right' }).waitFor({ state: 'detached' });
await page.getByRole('menuitem', { name: 'Table right' }).hover();
await page.waitForTimeout(300);
await page.getByRole('menuitem', { name: 'Associated records right' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();

View File

@ -751,6 +751,7 @@ test.describe('actions schema settings', () => {
await page
.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:popup-general' })
.hover();
await page.waitForTimeout(300);
};
test('supported options', async ({ page, mockPage, mockRecord }) => {

View File

@ -39,11 +39,11 @@ const enabledIndexColumn: SchemaSettingsItemType = {
const { dn } = useDesignable();
return {
title: t('Enable index column'),
checked: field.decoratorProps.enableIndexÏColumn !== false,
onChange: async (enableIndexÏColumn) => {
checked: field.decoratorProps.enableIndexColumn !== false,
onChange: async (enableIndexColumn) => {
field.decoratorProps = field.decoratorProps || {};
field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;
fieldSchema['x-decorator-props'].enableIndexÏColumn = enableIndexÏColumn;
field.decoratorProps.enableIndexColumn = enableIndexColumn;
fieldSchema['x-decorator-props'].enableIndexColumn = enableIndexColumn;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],

View File

@ -41,11 +41,11 @@ const enabledIndexColumn: SchemaSettingsItemType = {
const { dn } = useDesignable();
return {
title: t('Enable index column'),
checked: field.componentProps.enableIndexÏColumn !== false,
onChange: async (enableIndexÏColumn) => {
checked: field.componentProps.enableIndexColumn !== false,
onChange: async (enableIndexColumn) => {
field.componentProps = field.componentProps || {};
field.componentProps.enableIndexÏColumn = enableIndexÏColumn;
fieldSchema['x-component-props'].enableIndexÏColumn = enableIndexÏColumn;
field.componentProps.enableIndexColumn = enableIndexColumn;
fieldSchema['x-component-props'].enableIndexColumn = enableIndexColumn;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],

View File

@ -180,7 +180,6 @@ const layoutContentClass = css`
`;
const className1 = css`
width: 168px;
height: var(--nb-header-height);
margin-right: 4px;
display: inline-flex;
@ -189,6 +188,15 @@ const className1 = css`
padding: 0;
align-items: center;
`;
const className1WithFixedWidth = css`
${className1}
width: 168px;
`;
const className1WithAutoWidth = css`
${className1}
width: auto;
min-width: 168px;
`;
const className2 = css`
object-fit: contain;
width: 100%;
@ -266,7 +274,8 @@ const NocoBaseLogo = () => {
const { token } = useToken();
const fontSizeStyle = useMemo(() => ({ fontSize: token.fontSizeHeading3 }), [token.fontSizeHeading3]);
const logo = result?.data?.data?.logo?.url ? (
const hasLogo = result?.data?.data?.logo?.url;
const logo = hasLogo ? (
<img className={className2} src={result?.data?.data?.logo?.url} />
) : (
<span style={fontSizeStyle} className={className3}>
@ -274,7 +283,9 @@ const NocoBaseLogo = () => {
</span>
);
return <div className={className1}>{result?.loading ? null : logo}</div>;
return (
<div className={hasLogo ? className1WithFixedWidth : className1WithAutoWidth}>{result?.loading ? null : logo}</div>
);
};
/**
@ -318,6 +329,7 @@ const GroupItem: FC<{ item: any }> = (props) => {
{...item._route.options.badge}
count={badgeCount}
style={{ marginLeft: 4, color: item._route.options?.badge?.textColor }}
dot={false}
></Badge>
)}
</SortableItem>
@ -334,7 +346,7 @@ const WithTooltip: FC<{ title: string; hidden: boolean; badgeProps: any }> = (pr
{(context) =>
context.collapsed && !props.hidden && !inHeader ? (
<Tooltip title={props.title} placement="right">
<Badge {...props.badgeProps} style={{ transform: 'none' }}>
<Badge {...props.badgeProps} style={{ transform: 'none' }} dot={false}>
{props.children}
</Badge>
</Tooltip>
@ -427,6 +439,7 @@ const MenuItem: FC<{ item: any; options: { isMobile: boolean; collapsed: boolean
{...item._route.options?.badge}
count={badgeCount}
style={{ marginLeft: 4, color: item._route.options?.badge?.textColor }}
dot={false}
></Badge>
)}
</SortableItem>
@ -454,7 +467,11 @@ const MenuItem: FC<{ item: any; options: { isMobile: boolean; collapsed: boolean
</WithTooltip>
<MenuSchemaToolbar />
{badgeCount != null && (
<Badge {...badgeProps} style={{ marginLeft: 4, color: item._route.options?.badge?.textColor }}></Badge>
<Badge
{...badgeProps}
style={{ marginLeft: 4, color: item._route.options?.badge?.textColor }}
dot={false}
></Badge>
)}
</SortableItem>
</NocoBaseRouteContext.Provider>

View File

@ -147,6 +147,10 @@ const InternalActionBar: FC = (props: any) => {
export const ActionBar = withDynamicSchemaProps(
(props: any) => {
if (props.hidden) {
return null;
}
return <InternalActionBar {...props} />;
},
{ displayName: 'ActionBar' },

View File

@ -8,9 +8,9 @@
*/
import { Field } from '@formily/core';
import { observer, useField, useFieldSchema } from '@formily/react';
import { observer, useField, useFieldSchema, SchemaOptionsContext } from '@formily/react';
import _ from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useRef } from 'react';
import { useAPIClient, useRequest } from '../../../api-client';
import { useCollectionManager } from '../../../data-source/collection';
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
@ -18,6 +18,7 @@ import { getDataSourceHeaders } from '../../../data-source/utils';
import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive';
import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context';
import { FormItem, useSchemaOptionsContext } from '../../../schema-component';
export const AssociationFieldProvider = observer(
(props) => {
@ -25,6 +26,8 @@ export const AssociationFieldProvider = observer(
const cm = useCollectionManager();
const fieldSchema = useFieldSchema();
const api = useAPIClient();
const option = useSchemaOptionsContext();
const rootRef = useRef<HTMLDivElement>(null);
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
useSchemaComponentContext();
@ -151,13 +154,34 @@ export const AssociationFieldProvider = observer(
if (loading || rLoading) {
return null;
}
const components = {
...option.components,
FormItem: (props) => {
return (
<FormItem
{...props}
getPopupContainer={(triggerNode) => {
return rootRef.current || document.body;
}}
/>
);
},
};
return collectionField ? (
<div ref={rootRef}>
<AssociationFieldContext.Provider
value={{ options: collectionField, field, fieldSchema, allowMultiple, allowDissociate, currentMode }}
>
<SchemaOptionsContext.Provider
value={{
components,
scope: option.scope,
}}
>
{props.children}
</SchemaOptionsContext.Provider>
</AssociationFieldContext.Provider>
</div>
) : null;
},
{ displayName: 'AssociationFieldProvider' },

View File

@ -11,7 +11,7 @@ import { observer, useField, useFieldSchema } from '@formily/react';
import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client';
import { Select, Space } from 'antd';
import { differenceBy, unionBy } from 'lodash';
import React, { useContext, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import {
FormProvider,
PopupSettingsProvider,
@ -135,6 +135,11 @@ export const InternalPicker = observer(
const filter = list.length ? { $and: [{ [`${targetKey}.$ne`]: list }] } : {};
return filter;
};
useEffect(() => {
if (!value) {
setSelectedRows([]);
}
}, [value]);
const usePickActionProps = () => {
const { setVisible } = useActionContext();
const { multiple, selectedRows, onChange, options, collectionField } = useContext(RecordPickerContext);

View File

@ -107,7 +107,7 @@ export const SubTable: any = observer(
const labelUiSchema = useLabelUiSchema(collectionField, fieldNames?.label || 'label');
const recordV2 = useCollectionRecord();
const collection = useCollection();
const { allowSelectExistingRecord, allowAddnew, allowDisassociation, enableIndexÏColumn } = field.componentProps;
const { allowSelectExistingRecord, allowAddnew, allowDisassociation, enableIndexColumn } = field.componentProps;
useSubTableSpecialCase({ rootField: field, rootSchema: schema });
@ -263,7 +263,7 @@ export const SubTable: any = observer(
locale={{
emptyText: <span> {field.editable ? t('Please add or select record') : t('No data')}</span>,
}}
enableIndexÏColumn={enableIndexÏColumn !== false}
enableIndexColumn={enableIndexColumn !== false}
footer={() => (
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{field.editable && (

View File

@ -515,6 +515,7 @@ const InternalBodyCellComponent = (props) => {
const displayNone = { display: 'none' };
const BodyCellComponent = ({ columnHidden, ...props }) => {
const { designable } = useDesignable();
const collection = useCollection();
if (columnHidden) {
return (
@ -524,7 +525,11 @@ const BodyCellComponent = ({ columnHidden, ...props }) => {
);
}
return <InternalBodyCellComponent {...props} />;
return (
<SubFormProvider value={{ value: props?.record, collection, fieldSchema: props.schema }}>
<InternalBodyCellComponent {...props} />{' '}
</SubFormProvider>
);
};
interface TableProps {
@ -673,7 +678,7 @@ export const Table: any = withDynamicSchemaProps(
onExpand,
loading,
onClickRow,
enableIndexÏColumn,
enableIndexColumn,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
@ -826,7 +831,7 @@ export const Table: any = withDynamicSchemaProps(
const restProps = useMemo(
() => ({
rowSelection: enableIndexÏColumn
rowSelection: enableIndexColumn
? memoizedRowSelection
? {
type: 'checkbox',
@ -900,7 +905,7 @@ export const Table: any = withDynamicSchemaProps(
isRowSelect,
memoizedRowSelection,
paginationProps,
enableIndexÏColumn,
enableIndexColumn,
],
);

View File

@ -56,16 +56,23 @@ ReadPretty.Input = (props: InputReadPrettyProps) => {
return props.value && typeof props.value === 'object' ? JSON.stringify(props.value) : compile(props.value);
}, [props.value]);
const flexStyle = props.ellipsis ? { display: 'flex', alignItems: 'center' } : {};
return (
<div
className={cls(prefixCls, props.className)}
style={{ overflowWrap: 'break-word', whiteSpace: 'normal', ...props.style }}
style={{
...flexStyle,
overflowWrap: 'break-word',
whiteSpace: 'normal',
...props.style,
}}
>
{props.addonBefore}
{props.prefix}
{compile(props.addonBefore)}
{compile(props.prefix)}
{props.ellipsis ? <EllipsisWithTooltip ellipsis={props.ellipsis}>{content}</EllipsisWithTooltip> : content}
{props.suffix}
{props.addonAfter}
{compile(props.suffix)}
{compile(props.addonAfter)}
</div>
);
};

View File

@ -244,7 +244,7 @@ const TabBadge: FC<{ tabRoute: NocoBaseDesktopRoute; style?: React.CSSProperties
if (badgeCount == null) return null;
return (
<Badge {...props.tabRoute.options?.badge} count={badgeCount} style={props.style}>
<Badge {...props.tabRoute.options?.badge} count={badgeCount} style={props.style} dot={false}>
{props.children}
</Badge>
);

View File

@ -857,7 +857,7 @@ export const Table: any = withDynamicSchemaProps(
const collection = useCollection();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag, allIncludesChildren, enableIndexÏColumn } = ctx;
const { expandFlag, allIncludesChildren, enableIndexColumn } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2, props);
const columns = useTableColumns(others, paginationProps);
@ -1023,7 +1023,7 @@ export const Table: any = withDynamicSchemaProps(
const restProps = useMemo(
() => ({
rowSelection:
enableIndexÏColumn !== false
enableIndexColumn !== false
? memoizedRowSelection
? {
type: 'checkbox',
@ -1105,7 +1105,7 @@ export const Table: any = withDynamicSchemaProps(
memoizedRowSelection,
paginationProps,
tableBlockContextBasicValue,
enableIndexÏColumn,
enableIndexColumn,
],
);

View File

@ -182,10 +182,10 @@ export const TableBlockDesigner = () => {
<SchemaSettingsSwitchItem
title={t('Enable index column')}
checked={field.decoratorProps?.enableSelectColumn !== false}
onChange={async (enableIndexÏColumn) => {
onChange={async (enableIndexColumn) => {
field.decoratorProps = field.decoratorProps || {};
field.decoratorProps.enableIndexÏColumn = enableIndexÏColumn;
fieldSchema['x-decorator-props'].enableIndexÏColumn = enableIndexÏColumn;
field.decoratorProps.enableIndexColumn = enableIndexColumn;
fieldSchema['x-decorator-props'].enableIndexColumn = enableIndexColumn;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],

View File

@ -240,7 +240,7 @@ function useSizeHint(size: number) {
const s = size ?? FILE_SIZE_LIMIT_DEFAULT;
const { t, i18n } = useTranslation();
const sizeString = filesize(s, { base: 2, standard: 'jedec', locale: i18n.language });
return s !== 0 ? t('File size should not exceed {{size}}.', { size: 10000000 }) : '';
return s !== 0 ? t('File size should not exceed {{size}}.', { size: sizeString }) : '';
}
function DefaultThumbnailPreviewer({ file }) {

View File

@ -144,7 +144,7 @@ export const DateFilterDynamicComponent = (props) => {
<Select
key="unit"
value={value?.unit}
style={{ maxWidth: 140, minWidth: 130 }}
style={{ maxWidth: 140 }}
onChange={(val) => {
const obj = {
...value,
@ -158,6 +158,7 @@ export const DateFilterDynamicComponent = (props) => {
{ value: 'month', label: t('Calendar Month') },
{ value: 'year', label: t('Calendar Year') },
]}
popupMatchSelectWidth
/>,
]}
{(value?.type === 'exact' || !value?.type) && <SmartDatePicker {...props} />}

View File

@ -10,8 +10,9 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { merge } from '@formily/shared';
import _, { cloneDeep } from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import _, { cloneDeepWith } from 'lodash';
import React, { isValidElement, useCallback, useEffect, useMemo } from 'react';
import { useBlockContext } from '../../../block-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import {
useCollectionField_deprecated,
@ -20,14 +21,13 @@ import {
useCollection_deprecated,
} from '../../../collection-manager';
import { CollectionFieldProvider } from '../../../data-source';
import { FlagProvider } from '../../../flag-provider';
import { useRecord } from '../../../record-provider';
import { useCompile, useComponent } from '../../../schema-component';
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
import { Option } from '../../../schema-settings/VariableInput/type';
import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop';
import { useLocalVariables, useVariables } from '../../../variables';
import { useBlockContext } from '../../../block-provider';
import { FlagProvider } from '../../../flag-provider';
interface AssignedFieldProps {
value: any;
onChange: (value: any) => void;
@ -126,7 +126,14 @@ export const AssignedFieldInner = (props: AssignedFieldProps) => {
currentForm.children = formatVariableScop(currentFormFields);
}
return cloneDeep(scope);
return cloneDeepWith(scope, (value) => {
// 不对 `ReactElement` 进行深拷贝,因为会报错
if (isValidElement(value)) {
return value;
}
// 对于其他类型的对象,继续正常的深拷贝
return undefined;
});
},
[currentFormFields, name],
);

View File

@ -110,6 +110,7 @@ const quickEditField = [
'circle',
'point',
'lineString',
'vditor',
];
export function useTableColumnInitializerFields() {

View File

@ -83,14 +83,17 @@ export function bindLinkageRulesToFiled(
// 3. value 表达式中的变量值;
() => {
// 获取条件中的字段值
getFieldValuesInCondition({ linkageRules, formValues });
const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues });
// 获取条件中的变量值
getVariableValuesInCondition({ linkageRules, localVariables });
const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables });
// 获取 value 表达式中的变量值
getVariableValuesInExpression({ action, localVariables });
const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables });
return uid();
const result = [fieldValuesInCondition, variableValuesInCondition, variableValuesInExpression]
.map((item) => JSON.stringify(item))
.join(',');
return result;
},
getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }, jsonLogic),
{ fireImmediately: true, equals: _.isEqual },

View File

@ -34,7 +34,8 @@ export function useSatisfiedActionValues({
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
const localSchema = schema ?? fieldSchema;
const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
const styleRules =
rules ?? (localSchema[LinkageRuleDataKeyMap[category]] || localSchema?.parent[LinkageRuleDataKeyMap[category]]);
const app = useApp();
const compute = useCallback(() => {
@ -65,7 +66,7 @@ export function useSatisfiedActionValues({
form.removeEffects(id);
};
}
}, [form, compute]);
}, [form, compute, formValues]);
return { valueMap };
}

View File

@ -19,6 +19,7 @@ import { useParentPopupVariableContext } from '../../schema-settings/VariableInp
import { useCurrentParentRecordContext } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable';
import { usePopupVariableContext } from '../../schema-settings/VariableInput/hooks/usePopupVariable';
import { useCurrentRecordContext } from '../../schema-settings/VariableInput/hooks/useRecordVariable';
import { useURLSearchParamsVariable } from '../../schema-settings/VariableInput/hooks/useURLSearchParamsVariable';
import { VariableOption } from '../types';
import useContextVariable from './useContextVariable';
import { useApp } from '../../application/hooks/useApp';
@ -53,6 +54,7 @@ const useLocalVariables = (props?: Props) => {
dataSource: parentPopupDataSource,
defaultValue: defaultValueOfParentPopupRecord,
} = useParentPopupVariableContext();
const { urlSearchParamsCtx, shouldDisplay: shouldDisplayURLSearchParams } = useURLSearchParamsVariable();
const { datetimeCtx } = useDatetimeVariableContext();
const { currentFormCtx } = useCurrentFormContext({ form: props?.currentForm });
const { name: currentCollectionName } = useCollection_deprecated();
@ -156,6 +158,10 @@ const useLocalVariables = (props?: Props) => {
ctx: parentObjectCtx,
collectionName: collectionNameOfParentObject,
},
shouldDisplayURLSearchParams && {
name: '$nURLSearchParams',
ctx: urlSearchParamsCtx,
},
...customVariables,
] as VariableOption[]
).filter(Boolean);
@ -181,6 +187,7 @@ const useLocalVariables = (props?: Props) => {
parentObjectCtx,
collectionNameOfParentObject,
contextVariable,
urlSearchParamsCtx,
...customVariables.map((item) => item.ctx),
]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中
};

View File

@ -1,6 +1,6 @@
{
"name": "create-nocobase-app",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"main": "src/index.js",
"license": "AGPL-3.0",
"dependencies": {

View File

@ -1,16 +1,16 @@
{
"name": "@nocobase/data-source-manager",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"license": "AGPL-3.0",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"dependencies": {
"@nocobase/actions": "1.8.0-alpha.5",
"@nocobase/cache": "1.8.0-alpha.5",
"@nocobase/database": "1.8.0-alpha.5",
"@nocobase/resourcer": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/actions": "1.8.0-alpha.8",
"@nocobase/cache": "1.8.0-alpha.8",
"@nocobase/database": "1.8.0-alpha.8",
"@nocobase/resourcer": "1.8.0-alpha.8",
"@nocobase/utils": "1.8.0-alpha.8",
"@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^9.0.2"
},

View File

@ -45,11 +45,11 @@ const actions: Actions = {
method: 'destroy',
},
firstOrCreate: {
params: ['values', 'filterKeys'],
params: ['values', 'filterKeys', 'whitelist', 'blacklist', 'updateAssociationValues', 'targetCollection'],
method: 'firstOrCreate',
},
updateOrCreate: {
params: ['values', 'filterKeys'],
params: ['values', 'filterKeys', 'whitelist', 'blacklist', 'updateAssociationValues', 'targetCollection'],
method: 'updateOrCreate',
},
remove: {

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/database",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@nocobase/logger": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/logger": "1.8.0-alpha.8",
"@nocobase/utils": "1.8.0-alpha.8",
"async-mutex": "^0.3.2",
"chalk": "^4.1.1",
"cron-parser": "4.4.0",

View File

@ -70,4 +70,32 @@ describe('eq operator', () => {
expect(results).toEqual(0);
});
it('should eq string field with number value', async () => {
await db.getRepository('tests').create({
values: [{ name: '123' }, { name: '234' }, { name: '345' }],
});
const results = await db.getRepository('tests').count({
filter: {
'name.$eq': 123,
},
});
expect(results).toEqual(1);
});
it('should eq string field with number value (array)', async () => {
await db.getRepository('tests').create({
values: [{ name: '123' }, { name: '234' }, { name: '345' }],
});
const results = await db.getRepository('tests').count({
filter: {
'name.$eq': [123],
},
});
expect(results).toEqual(1);
});
});

View File

@ -30,6 +30,7 @@ export class BelongsToArrayAssociation {
targetKey: string;
identifierField: string;
as: string;
options: any;
constructor(options: {
db: Database;
@ -40,6 +41,7 @@ export class BelongsToArrayAssociation {
targetKey: string;
}) {
const { db, source, as, foreignKey, target, targetKey } = options;
this.options = options;
this.associationType = 'BelongsToArray';
this.db = db;
this.source = source;

View File

@ -14,6 +14,11 @@ export default {
if (ctx?.fieldPath) {
const field = ctx.db.getFieldByPath(ctx.fieldPath);
if (field?.type === 'string' && typeof val !== 'string') {
if (Array.isArray(val)) {
return {
[Op.in]: val.map((v) => String(v)),
};
}
return {
[Op.eq]: String(val),
};

View File

@ -26,7 +26,10 @@ import {
WhereOperators,
} from 'sequelize';
import _ from 'lodash';
import { BelongsToArrayRepository } from './belongs-to-array/belongs-to-array-repository';
import { Collection } from './collection';
import { SmartCursorBuilder } from './cursor-builder';
import { Database } from './database';
import mustHaveFilter from './decorators/must-have-filter-decorator';
import injectTargetCollection from './decorators/target-collection-decorator';
@ -38,7 +41,6 @@ import FilterParser from './filter-parser';
import { Model } from './model';
import operators from './operators';
import { OptionsParser } from './options-parser';
import { BelongsToArrayRepository } from './belongs-to-array/belongs-to-array-repository';
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
import { HasManyRepository } from './relation-repository/hasmany-repository';
@ -47,8 +49,6 @@ import { RelationRepository } from './relation-repository/relation-repository';
import { updateAssociations, updateModelByValues } from './update-associations';
import { UpdateGuard } from './update-guard';
import { valuesToFilter } from './utils/filter-utils';
import _ from 'lodash';
import { SmartCursorBuilder } from './cursor-builder';
const debug = require('debug')('noco-database');
@ -242,6 +242,7 @@ export interface FirstOrCreateOptions extends Transactionable {
values?: Values;
hooks?: boolean;
context?: any;
updateAssociationValues?: AssociationKeysToBeUpdate;
}
export class Repository<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes> {
@ -521,7 +522,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
* Get the first record matching the attributes or create it.
*/
async firstOrCreate(options: FirstOrCreateOptions) {
const { filterKeys, values, transaction, hooks, context } = options;
const { filterKeys, values, transaction, context, ...rest } = options;
const filter = Repository.valuesToFilter(values, filterKeys);
const instance = await this.findOne({ filter, transaction, context });
@ -530,11 +531,12 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
return instance;
}
return this.create({ values, transaction, hooks, context });
return this.create({ values, transaction, context, ...rest });
}
async updateOrCreate(options: FirstOrCreateOptions) {
const { filterKeys, values, transaction, hooks, context } = options;
const { filterKeys, values, transaction, context, ...rest } = options;
const filter = Repository.valuesToFilter(values, filterKeys);
const instance = await this.findOne({ filter, transaction, context });
@ -544,12 +546,12 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
filterByTk: instance.get(this.collection.filterTargetKey || this.collection.model.primaryKeyAttribute),
values,
transaction,
hooks,
context,
...rest,
});
}
return this.create({ values, transaction, hooks, context });
return this.create({ values, transaction, context, ...rest });
}
/**

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/devtools",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"license": "AGPL-3.0",
"main": "./src/index.js",
"dependencies": {
"@nocobase/build": "1.8.0-alpha.5",
"@nocobase/client": "1.8.0-alpha.5",
"@nocobase/test": "1.8.0-alpha.5",
"@nocobase/build": "1.8.0-alpha.8",
"@nocobase/client": "1.8.0-alpha.8",
"@nocobase/test": "1.8.0-alpha.8",
"@types/koa": "^2.15.0",
"@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177",

View File

@ -1,13 +1,13 @@
{
"name": "@nocobase/evaluators",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@formulajs/formulajs": "4.4.9",
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.8",
"mathjs": "^10.6.0"
},
"repository": {

View File

@ -92,9 +92,6 @@ const FlowModelRendererWithAutoFlows: React.FC<{
}) => {
const defaultExtraContext = useFlowExtraContext();
useApplyAutoFlows(model, extraContext || defaultExtraContext, !independentAutoFlowExecution);
useEffect(() => {
model.setSharedContext(sharedContext);
}, [sharedContext]);
return (
<FlowModelRendererCore
@ -117,10 +114,6 @@ const FlowModelRendererWithoutAutoFlows: React.FC<{
hideRemoveInSettings: boolean;
sharedContext?: Record<string, any>;
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, sharedContext }) => {
useEffect(() => {
model.setSharedContext(sharedContext);
}, [sharedContext]);
return (
<FlowModelRendererCore
model={model}
@ -217,6 +210,10 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
return null;
}
useEffect(() => {
model.setSharedContext(sharedContext);
}, [model, sharedContext]);
// 根据 skipApplyAutoFlows 选择不同的内部组件
if (skipApplyAutoFlows) {
return (

View File

@ -13,6 +13,26 @@ import React from 'react';
import { ActionStepDefinition } from '../../../../types';
import { resolveDefaultParams } from '../../../../utils';
/**
*
* @param uiSchema UI Schema
* @param currentParams
* @returns
*/
function hasRequiredParams(uiSchema: Record<string, any>, currentParams: Record<string, any>): boolean {
// 检查 uiSchema 中所有 required 为 true 的字段
for (const [fieldKey, fieldSchema] of Object.entries(uiSchema)) {
if (fieldSchema.required === true) {
// 如果字段是必需的,但当前参数中没有值或值为空
const value = currentParams[fieldKey];
if (value === undefined || value === null || value === '') {
return false;
}
}
}
return true;
}
const SchemaField = createSchemaField();
/**
@ -87,8 +107,16 @@ const openRequiredParamsStepFormDialog = async ({
}
});
// 如果有可配置的UI Schema添加到列表中
// 如果有可配置的UI Schema检查是否已经有了所需的配置值
if (Object.keys(mergedUiSchema).length > 0) {
// 获取当前步骤的参数
const currentStepParams = model.getStepParams(flowKey, stepKey) || {};
// 检查是否已经有了所需的配置值
const hasAllRequiredParams = hasRequiredParams(mergedUiSchema, currentStepParams);
// 只有当缺少必需参数时才添加到列表中
if (!hasAllRequiredParams) {
requiredSteps.push({
flowKey,
stepKey,
@ -101,6 +129,7 @@ const openRequiredParamsStepFormDialog = async ({
}
}
}
}
// 如果没有需要配置的步骤,显示提示
if (requiredSteps.length === 0) {
@ -143,7 +172,7 @@ const openRequiredParamsStepFormDialog = async ({
// 构建分步表单的 Schema
const stepPanes: Record<string, any> = {};
requiredSteps.forEach(({ flowKey, stepKey, uiSchema, title, flowTitle }, index) => {
requiredSteps.forEach(({ flowKey, stepKey, uiSchema, title, flowTitle }) => {
const stepId = `${flowKey}_${stepKey}`;
stepPanes[stepId] = {
@ -267,7 +296,7 @@ const openRequiredParamsStepFormDialog = async ({
formStep.next();
}
})
.catch((errors) => {
.catch((errors: any) => {
console.log('表单验证失败:', errors);
// 可以在这里添加更详细的错误处理
});
@ -316,7 +345,7 @@ const openRequiredParamsStepFormDialog = async ({
formStep.next();
}
})
.catch((errors) => {
.catch((errors: any) => {
console.log('表单验证失败:', errors);
// 可以在这里添加更详细的错误处理
});

View File

@ -8,7 +8,7 @@
*/
import React, { useMemo } from 'react';
import { AddSubModelButton } from './AddSubModelButton';
import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton';
import { FlowModel } from '../../models/flowModel';
import { ModelConstructor } from '../../types';
import { Button } from 'antd';
@ -32,6 +32,14 @@ interface AddActionButtonProps {
*
*/
children?: React.ReactNode;
/**
* Model菜单的函数
*/
filter?: (blockClass: ModelConstructor, className: string) => boolean;
/**
* itemsaction菜单
*/
items?: SubModelItemsType;
}
/**
@ -51,12 +59,17 @@ export const AddActionButton: React.FC<AddActionButtonProps> = ({
subModelKey = 'actions',
children = <Button>Configure actions</Button>,
subModelType = 'array',
items,
filter,
onModelAdded,
}) => {
const items = useMemo(() => {
const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
const allActionsItems = useMemo(() => {
const actionClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
const registeredBlocks = [];
for (const [className, ModelClass] of blockClasses) {
for (const [className, ModelClass] of actionClasses) {
if (filter && !filter(ModelClass, className)) {
continue;
}
const item = {
key: className,
label: ModelClass.meta?.title || className,
@ -76,7 +89,7 @@ export const AddActionButton: React.FC<AddActionButtonProps> = ({
model={model}
subModelKey={subModelKey}
subModelType={subModelType}
items={items}
items={items ?? allActionsItems}
onModelAdded={onModelAdded}
>
{children}

View File

@ -8,10 +8,11 @@
*/
import React, { useMemo } from 'react';
import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton';
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton';
import { FlowModel } from '../../models/flowModel';
import { ModelConstructor } from '../../types';
import { Button } from 'antd';
import { createBlockItems } from './blockItems';
interface AddBlockButtonProps {
/**
@ -33,19 +34,42 @@ interface AddBlockButtonProps {
*/
children?: React.ReactNode;
/**
* items
* items
*/
items?: SubModelItemsType;
/**
* Model菜单的函数
*/
filter?: (blockClass: ModelConstructor, className: string) => boolean;
/**
*
*/
appendItems?: SubModelItemsType;
}
/**
*
*
* page:addBlock -> ->
*
* @example
* ```tsx
* // 基本用法
* <AddBlockButton
* model={parentModel}
* subModelBaseClass={'FlowModel'}
* subModelBaseClass={'BlockFlowModel'}
* />
*
* // 追加自定义菜单项
* <AddBlockButton
* model={parentModel}
* appendItems={[
* {
* key: 'customBlock',
* label: 'Custom Block',
* createModelOptions: { use: 'CustomBlock' }
* }
* ]}
* />
* ```
*/
@ -56,31 +80,32 @@ export const AddBlockButton: React.FC<AddBlockButtonProps> = ({
children = <Button>Add block</Button>,
subModelType = 'array',
items,
filter: filterBlocks,
appendItems,
onModelAdded,
}) => {
const defaultItems = useMemo(() => {
const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
const registeredBlocks = [];
for (const [className, ModelClass] of blockClasses) {
registeredBlocks.push({
key: className,
label: ModelClass.meta?.title || className,
icon: ModelClass.meta?.icon,
createModelOptions: {
...ModelClass.meta?.defaultOptions,
use: className,
},
});
// 确定最终使用的 items
const finalItems = useMemo(() => {
if (items) {
// 如果明确提供了 items直接使用
return items;
}
return registeredBlocks;
}, [model, subModelBaseClass]);
// 创建区块菜单项,并合并追加的 items
const blockItems = createBlockItems(model, {
subModelBaseClass,
filterBlocks,
});
return mergeSubModelItems([blockItems, appendItems]);
}, [items, model, subModelBaseClass, filterBlocks, appendItems]);
return (
<AddSubModelButton
model={model}
subModelKey={subModelKey}
subModelType={subModelType}
items={items || defaultItems}
items={finalItems}
onModelAdded={onModelAdded}
>
{children}

View File

@ -42,6 +42,10 @@ export interface AddFieldButtonProps {
* UI组件
*/
children?: React.ReactNode;
/**
* items
*/
items?: SubModelItemsType;
}
/**
@ -63,6 +67,7 @@ export const AddFieldButton: React.FC<AddFieldButtonProps> = ({
subModelType = 'array',
collection,
buildCreateModelOptions,
items,
appendItems,
onModelAdded,
}) => {
@ -117,7 +122,7 @@ export const AddFieldButton: React.FC<AddFieldButtonProps> = ({
};
}, [model, subModelBaseClass, fields, buildCreateModelOptions]);
const items = useMemo(() => {
const fieldItems = useMemo(() => {
return mergeSubModelItems([buildFieldItems, appendItems], { addDividers: true });
}, [buildFieldItems, appendItems]);
@ -126,7 +131,7 @@ export const AddFieldButton: React.FC<AddFieldButtonProps> = ({
model={model}
subModelKey={subModelKey}
subModelType={subModelType}
items={items}
items={items ?? fieldItems}
onModelAdded={onModelAdded}
>
{children}

View File

@ -0,0 +1,189 @@
/**
* 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 { FlowModel } from '../../models/flowModel';
import { ModelConstructor } from '../../types';
import { SubModelItem, SubModelItemsType } from './AddSubModelButton';
import { DataSource, DataSourceManager, Collection } from '../../data-source';
export interface BlockItemsOptions {
/**
*
*/
subModelBaseClass?: string | ModelConstructor;
/**
*
*/
filterBlocks?: (blockClass: ModelConstructor, className: string) => boolean;
/**
*
*/
customBlocks?: SubModelItem[];
}
/**
*
* flowEngine
*/
async function getDataSourcesWithCollections(model: FlowModel) {
try {
// 从 flowEngine 的全局上下文获取数据源管理器
const globalContext = model.flowEngine.getContext();
const dataSourceManager: DataSourceManager = globalContext?.dataSourceManager;
if (!dataSourceManager) {
// 如果没有数据源管理器,返回空数组
return [];
}
// 获取所有数据源
const allDataSources: DataSource[] = dataSourceManager.getDataSources();
// 转换为我们需要的格式
return allDataSources.map((dataSource: DataSource) => {
const key = dataSource.name;
const displayName = dataSource.options.displayName || dataSource.name;
// 从 collectionManager 获取 collections
const collections: Collection[] = dataSource.getCollections();
return {
key,
displayName,
collections: collections.map((collection: Collection) => ({
name: collection.name,
title: collection.title,
dataSource: key,
})),
};
});
} catch (error) {
console.warn('Failed to get data sources:', error);
// 返回空数组,不提供假数据
return [];
}
}
/**
*
*
*
* -
* -
*
* @param model FlowModel
* @param options
* @returns SubModelItemsType
*/
export function createBlockItems(model: FlowModel, options: BlockItemsOptions = {}): SubModelItemsType {
const { subModelBaseClass = 'BlockFlowModel', filterBlocks, customBlocks = [] } = options;
// 获取所有注册的区块类
const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
// 分类区块:数据区块 vs 其他区块
const dataBlocks: Array<{ className: string; ModelClass: ModelConstructor }> = [];
const otherBlocks: Array<{ className: string; ModelClass: ModelConstructor }> = [];
for (const [className, ModelClass] of blockClasses) {
// 应用过滤器
if (filterBlocks && !filterBlocks(ModelClass, className)) {
continue;
}
// 判断是否为数据区块
const meta = (ModelClass as any).meta;
const isDataBlock =
meta?.category === 'data' ||
meta?.requiresDataSource === true ||
className.toLowerCase().includes('table') ||
className.toLowerCase().includes('form') ||
className.toLowerCase().includes('details') ||
className.toLowerCase().includes('list') ||
className.toLowerCase().includes('grid');
if (isDataBlock) {
dataBlocks.push({ className, ModelClass });
} else {
otherBlocks.push({ className, ModelClass });
}
}
const result: SubModelItem[] = [];
// 数据区块分组
if (dataBlocks.length > 0) {
result.push({
key: 'dataBlocks',
label: 'Data blocks',
type: 'group',
children: async () => {
const dataSources = await getDataSourcesWithCollections(model);
// 按区块类型组织菜单:区块 → 数据源 → 数据表
return dataBlocks.map(({ className, ModelClass }) => {
const meta = (ModelClass as any).meta;
return {
key: className,
label: meta?.title || className,
icon: meta?.icon,
children: dataSources.map((dataSource) => ({
key: `${className}.${dataSource.key}`,
label: dataSource.displayName,
children: dataSource.collections.map((collection) => ({
key: `${className}.${dataSource.key}.${collection.name}`,
label: collection.title || collection.name,
createModelOptions: {
...meta?.defaultOptions,
use: className,
stepParams: {
default: {
step1: {
dataSourceKey: dataSource.key,
collectionName: collection.name,
},
},
},
},
})),
})),
};
});
},
});
}
// 其他区块分组
if (otherBlocks.length > 0 || customBlocks.length > 0) {
const otherBlockItems = [
...otherBlocks.map(({ className, ModelClass }) => {
const meta = (ModelClass as any).meta;
return {
key: className,
label: meta?.title || className,
icon: meta?.icon,
createModelOptions: {
...meta?.defaultOptions,
use: className,
},
};
}),
...customBlocks,
];
result.push({
key: 'otherBlocks',
label: 'Other blocks',
type: 'group',
children: otherBlockItems,
});
}
return result;
}

View File

@ -12,4 +12,5 @@ export * from './AddBlockButton';
export * from './AddFieldButton';
export * from './AddSubModel';
export * from './AddSubModelButton';
export * from './blockItems';
//

View File

@ -227,13 +227,13 @@ export class FlowEngine {
async loadModel<T extends FlowModel = FlowModel>(uid: string): Promise<T | null> {
if (!this.ensureModelRepository()) return;
const data = await this.modelRepository.load(uid);
const data = await this.modelRepository.findOne({ uid });
return data?.uid ? this.createModel<T>(data as any) : null;
}
async loadOrCreateModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
if (!this.ensureModelRepository()) return;
const data = await this.modelRepository.load(options.uid);
const data = await this.modelRepository.findOne(options);
if (data?.uid) {
return this.createModel<T>(data as any);
} else {

View File

@ -14,7 +14,6 @@ import { uid } from 'uid/secure';
import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
import { FlowEngine } from '../flowEngine';
import { FlowExitException, resolveDefaultParams } from '../utils';
import type {
ActionStepDefinition,
ArrayElementType,
@ -30,7 +29,7 @@ import type {
StepParams,
} from '../types';
import { ExtendedFlowDefinition, FlowExtraContext, IModelComponentProps, ReadonlyModelProps } from '../types';
import { generateUid, mergeFlowDefinitions } from '../utils';
import { FlowExitException, generateUid, mergeFlowDefinitions, resolveDefaultParams } from '../utils';
import { ForkFlowModel } from './forkFlowModel';
// 使用WeakMap存储每个类的meta
@ -47,6 +46,8 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
public flowEngine: FlowEngine;
public parent: Structure['parent'];
public subModels: Structure['subModels'];
private _options: FlowModelOptions<Structure>;
/**
* fork
* 使 Set 便 dispose
@ -60,29 +61,23 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
// model 树的共享运行上下文
private _sharedContext: Record<string, any> = {};
public setSharedContext(ctx: Record<string, any>) {
this._sharedContext = ctx;
}
public getSharedContext() {
return {
...this.parent?.getSharedContext(),
...this._sharedContext, // 当前实例的 context 优先级最高
};
}
constructor(protected options: FlowModelOptions<Structure>) {
constructor(options: FlowModelOptions<Structure>) {
if (options?.flowEngine?.getModel(options.uid)) {
// 此时 new FlowModel 并不创建新实例而是返回已存在的实例避免重复创建同一个model实例
return options.flowEngine.getModel(options.uid);
}
this.uid = options.uid || uid();
if (!options.uid) {
options.uid = uid();
}
this.uid = options.uid;
this.props = options.props || {};
this.stepParams = options.stepParams || {};
this.subModels = {};
this.flowEngine = options.flowEngine;
this.sortIndex = options.sortIndex || 0;
this._options = options;
define(this, {
props: observable,
@ -555,7 +550,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
throw new Error('Parent must be an instance of FlowModel.');
}
this.parent = parent;
this.options.parentId = parent.uid;
this._options.parentId = parent.uid;
}
addSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
@ -731,11 +726,22 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
});
}
public setSharedContext(ctx: Record<string, any>) {
this._sharedContext = ctx;
}
public getSharedContext() {
return {
...this.parent?.getSharedContext(),
...this._sharedContext, // 当前实例的 context 优先级最高
};
}
// TODO: 不完整,需要考虑 sub-model 的情况
serialize(): Record<string, any> {
const data = {
uid: this.uid,
..._.omit(this.options, ['flowEngine']),
..._.omit(this._options, ['flowEngine']),
props: this.props,
stepParams: this.stepParams,
sortIndex: this.sortIndex,

View File

@ -248,7 +248,7 @@ export interface CreateModelOptions {
[key: string]: any; // 允许额外的自定义选项
}
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
load(uid: string): Promise<Record<string, any> | null>;
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
save(model: T): Promise<Record<string, any>>;
destroy(uid: string): Promise<boolean>;
}

View File

@ -8,8 +8,8 @@
*/
import _ from 'lodash';
import { DeepPartial, ModelConstructor, FlowDefinition, ParamsContext, FlowContext } from './types';
import type { FlowModel } from './models';
import { ActionDefinition, DeepPartial, FlowContext, FlowDefinition, ModelConstructor, ParamsContext } from './types';
export function generateUid(): string {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
@ -118,3 +118,7 @@ export class FlowExitException extends Error {
this.modelUid = modelUid;
}
}
export function defineAction(options: ActionDefinition) {
return options;
}

View File

@ -1,10 +1,10 @@
{
"name": "@nocobase/lock-manager",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"main": "lib/index.js",
"license": "AGPL-3.0",
"devDependencies": {
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.8",
"async-mutex": "^0.5.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/logger",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "nocobase logging library",
"license": "AGPL-3.0",
"main": "./lib/index.js",

View File

@ -1,12 +1,12 @@
{
"name": "@nocobase/resourcer",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
"dependencies": {
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.8",
"deepmerge": "^4.2.2",
"koa-compose": "^4.1.0",
"lodash": "^4.17.21",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/sdk",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"license": "AGPL-3.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/server",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",
@ -10,19 +10,19 @@
"@koa/cors": "^5.0.0",
"@koa/multer": "^3.1.0",
"@koa/router": "^13.1.0",
"@nocobase/acl": "1.8.0-alpha.5",
"@nocobase/actions": "1.8.0-alpha.5",
"@nocobase/auth": "1.8.0-alpha.5",
"@nocobase/cache": "1.8.0-alpha.5",
"@nocobase/data-source-manager": "1.8.0-alpha.5",
"@nocobase/database": "1.8.0-alpha.5",
"@nocobase/evaluators": "1.8.0-alpha.5",
"@nocobase/lock-manager": "1.8.0-alpha.5",
"@nocobase/logger": "1.8.0-alpha.5",
"@nocobase/resourcer": "1.8.0-alpha.5",
"@nocobase/sdk": "1.8.0-alpha.5",
"@nocobase/telemetry": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/acl": "1.8.0-alpha.8",
"@nocobase/actions": "1.8.0-alpha.8",
"@nocobase/auth": "1.8.0-alpha.8",
"@nocobase/cache": "1.8.0-alpha.8",
"@nocobase/data-source-manager": "1.8.0-alpha.8",
"@nocobase/database": "1.8.0-alpha.8",
"@nocobase/evaluators": "1.8.0-alpha.8",
"@nocobase/lock-manager": "1.8.0-alpha.8",
"@nocobase/logger": "1.8.0-alpha.8",
"@nocobase/resourcer": "1.8.0-alpha.8",
"@nocobase/sdk": "1.8.0-alpha.8",
"@nocobase/telemetry": "1.8.0-alpha.8",
"@nocobase/utils": "1.8.0-alpha.8",
"@types/decompress": "4.2.7",
"@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3",

View File

@ -22,10 +22,14 @@ export default (app: Application) => {
const Collection = app.db.getCollection('collections');
if (Collection) {
// @ts-ignore
await Collection.repository.setApp(app);
// @ts-ignore
await Collection.repository.load();
}
app.log.info('syncing database...');
const force = false;
await app.db.sync({
force,

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/telemetry",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"description": "nocobase telemetry library",
"license": "AGPL-3.0",
"main": "./lib/index.js",
@ -11,7 +11,7 @@
"directory": "packages/telemetry"
},
"dependencies": {
"@nocobase/utils": "1.8.0-alpha.5",
"@nocobase/utils": "1.8.0-alpha.8",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/resources": "^1.19.0",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/test",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"main": "lib/index.js",
"module": "./src/index.ts",
"types": "./lib/index.d.ts",
@ -51,7 +51,7 @@
},
"dependencies": {
"@faker-js/faker": "8.1.0",
"@nocobase/server": "1.8.0-alpha.5",
"@nocobase/server": "1.8.0-alpha.8",
"@playwright/test": "^1.45.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "@nocobase/utils",
"version": "1.8.0-alpha.5",
"version": "1.8.0-alpha.8",
"main": "lib/index.js",
"types": "./lib/index.d.ts",
"license": "AGPL-3.0",

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