mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
Merge branch '2.0' into 2.0-ai
This commit is contained in:
commit
7a8b1fe22a
2
.github/workflows/build-docker.yml
vendored
2
.github/workflows/build-docker.yml
vendored
@ -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
|
||||
|
58
.github/workflows/build-pr-docker.yml
vendored
Normal file
58
.github/workflows/build-pr-docker.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: Build pr docker
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
push-docker:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
verdaccio:
|
||||
image: verdaccio/verdaccio:5
|
||||
ports:
|
||||
- 4873:4873
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: network=host
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Aliyun Container Registry (Public)
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}
|
||||
username: ${{ secrets.ALI_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.ALI_DOCKER_PASSWORD }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
nocobase/nocobase
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
VERDACCIO_URL=http://localhost:4873/
|
||||
COMMIT_HASH=${GITHUB_SHA}
|
||||
push: true
|
||||
tags: ${{ secrets.ALI_DOCKER_PUBLIC_REGISTRY }}/nocobase/nocobase:2.0.0-alpha
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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
|
||||
|
142
CHANGELOG.md
142
CHANGELOG.md
@ -5,6 +5,148 @@ 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.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[client]**
|
||||
- incorrect range limitation on date fields with time ([#7107](https://github.com/nocobase/nocobase/pull/7107)) by @katherinehhh
|
||||
|
||||
- When URL query parameter variables are empty, the data scope conditions are not removed ([#7104](https://github.com/nocobase/nocobase/pull/7104)) by @zhangzhonghe
|
||||
|
||||
- **[Mobile]** Fix mobile popup z-index issue ([#7110](https://github.com/nocobase/nocobase/pull/7110)) by @zhangzhonghe
|
||||
|
||||
- **[Calendar]** date field issue in quick create form of calendar block ([#7106](https://github.com/nocobase/nocobase/pull/7106)) by @katherinehhh
|
||||
|
||||
## [v1.7.16](https://github.com/nocobase/nocobase/compare/v1.7.15...v1.7.16) - 2025-06-19
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[Workflow]**
|
||||
- Fix incorrectly executed checking on big integer number ([#7099](https://github.com/nocobase/nocobase/pull/7099)) by @mytharcher
|
||||
|
||||
- Fix stats cascade deleted by non-current workflow version ([#7103](https://github.com/nocobase/nocobase/pull/7103)) by @mytharcher
|
||||
|
||||
- **[Action: Import records]** Resolve login failure issue after batch import of usernames and passwords ([#7076](https://github.com/nocobase/nocobase/pull/7076)) by @aaaaaajie
|
||||
|
||||
- **[Workflow: Approval]** Only participants can view (get) detail of approval by @mytharcher
|
||||
|
||||
## [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
|
||||
|
@ -5,6 +5,148 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||
|
||||
## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[client]**
|
||||
- 修复日期字段在含时间格式下的范围约束错误 ([#7107](https://github.com/nocobase/nocobase/pull/7107)) by @katherinehhh
|
||||
|
||||
- URL 查询参数变量为空时,数据范围的条件没有被移除 ([#7104](https://github.com/nocobase/nocobase/pull/7104)) by @zhangzhonghe
|
||||
|
||||
- **[移动端]** 修复移动端弹窗的层级问题 ([#7110](https://github.com/nocobase/nocobase/pull/7110)) by @zhangzhonghe
|
||||
|
||||
- **[日历]** 修复日历区块快速创建事项时,表单日期字段异常问题 ([#7106](https://github.com/nocobase/nocobase/pull/7106)) by @katherinehhh
|
||||
|
||||
## [v1.7.16](https://github.com/nocobase/nocobase/compare/v1.7.15...v1.7.16) - 2025-06-19
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[工作流]**
|
||||
- 修复已执行数在大整型数时检查错误的问题 ([#7099](https://github.com/nocobase/nocobase/pull/7099)) by @mytharcher
|
||||
|
||||
- 修复统计数据被不是主版本的工作流级联删除的问题 ([#7103](https://github.com/nocobase/nocobase/pull/7103)) by @mytharcher
|
||||
|
||||
- **[操作:导入记录]** 修复批量导入用户名和密码后无法登录的问题 ([#7076](https://github.com/nocobase/nocobase/pull/7076)) by @aaaaaajie
|
||||
|
||||
- **[工作流:审批]** 限制只有参与者可以查看审批详情 by @mytharcher
|
||||
|
||||
## [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
|
||||
|
||||
- 修复子表格中 Markdown(Vditor)字段组件渲染不正确的问题 ([#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
|
||||
|
||||
### 🐛 修复
|
||||
|
@ -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 "$@"
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/acl",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@nocobase/utils": "1.8.0-alpha.9",
|
||||
"minimatch": "^5.1.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@nocobase/actions",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@nocobase/database": "1.8.0-alpha.9",
|
||||
"@nocobase/resourcer": "1.8.0-alpha.9"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@nocobase/app",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@nocobase/preset-nocobase": "1.8.0-alpha.9",
|
||||
"@nocobase/server": "1.8.0-alpha.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "1.8.0-alpha.5"
|
||||
"@nocobase/client": "1.8.0-alpha.9"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/auth",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@nocobase/cache": "1.8.0-alpha.9",
|
||||
"@nocobase/database": "1.8.0-alpha.9",
|
||||
"@nocobase/resourcer": "1.8.0-alpha.9",
|
||||
"@nocobase/utils": "1.8.0-alpha.9",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/build",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"description": "Library build tool based on rollup.",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
4
packages/core/cache/package.json
vendored
4
packages/core/cache/package.json
vendored
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/cache",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"bloom-filters": "^3.0.1",
|
||||
"cache-manager": "^5.2.4",
|
||||
"cache-manager-redis-yet": "^4.1.2"
|
||||
|
15
packages/core/cache/src/__tests__/cache.test.ts
vendored
15
packages/core/cache/src/__tests__/cache.test.ts
vendored
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cli",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@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.9"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ console.log('process.env.DOC_LANG', lang);
|
||||
|
||||
export default defineConfig({
|
||||
hash: true,
|
||||
mfsu:false,
|
||||
alias: {
|
||||
...umiConfig.alias,
|
||||
},
|
||||
@ -82,6 +83,10 @@ export default defineConfig({
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
{
|
||||
title: 'Quickstart',
|
||||
link: '/core/flow-models/quickstart',
|
||||
},
|
||||
{
|
||||
title: 'FlowEngine',
|
||||
type: 'group',
|
||||
@ -140,10 +145,6 @@ export default defineConfig({
|
||||
title: 'Flow Models',
|
||||
type: 'group',
|
||||
children: [
|
||||
{
|
||||
title: 'Quickstart',
|
||||
link: '/core/flow-models/quickstart',
|
||||
},
|
||||
{
|
||||
title: 'Overview',
|
||||
link: '/core/flow-models',
|
||||
|
@ -1,198 +0,0 @@
|
||||
[
|
||||
{
|
||||
"key": "h7b9i8khc3q",
|
||||
"name": "users",
|
||||
"inherit": false,
|
||||
"hidden": false,
|
||||
"description": null,
|
||||
"category": [],
|
||||
"namespace": "users.users",
|
||||
"duplicator": {
|
||||
"dumpable": "optional",
|
||||
"with": "rolesUsers"
|
||||
},
|
||||
"sortable": "sort",
|
||||
"model": "UserModel",
|
||||
"createdBy": true,
|
||||
"updatedBy": true,
|
||||
"logging": true,
|
||||
"from": "db2cm",
|
||||
"title": "{{t(\"Users\")}}",
|
||||
"rawTitle": "{{t(\"Users\")}}",
|
||||
"fields": [
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "number",
|
||||
"title": "{{t(\"ID\")}}",
|
||||
"x-component": "InputNumber",
|
||||
"x-read-pretty": true,
|
||||
"rawTitle": "{{t(\"ID\")}}"
|
||||
},
|
||||
"key": "ffp1f2sula0",
|
||||
"name": "id",
|
||||
"type": "bigInt",
|
||||
"interface": "id",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"autoIncrement": true,
|
||||
"primaryKey": true,
|
||||
"allowNull": false
|
||||
},
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Nickname\")}}",
|
||||
"x-component": "Input",
|
||||
"rawTitle": "{{t(\"Nickname\")}}"
|
||||
},
|
||||
"key": "vrv7yjue90g",
|
||||
"name": "nickname",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
},
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Username\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": {
|
||||
"username": true
|
||||
},
|
||||
"required": true,
|
||||
"rawTitle": "{{t(\"Username\")}}"
|
||||
},
|
||||
"key": "2ccs6evyrub",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Email\")}}",
|
||||
"x-component": "Input",
|
||||
"x-validator": "email",
|
||||
"required": true,
|
||||
"rawTitle": "{{t(\"Email\")}}"
|
||||
},
|
||||
"key": "rrskwjl5wt1",
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"interface": "email",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"key": "t09bauwm0wb",
|
||||
"name": "roles",
|
||||
"type": "belongsToMany",
|
||||
"interface": "m2m",
|
||||
"description": null,
|
||||
"collectionName": "users",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"target": "roles",
|
||||
"foreignKey": "userId",
|
||||
"otherKey": "roleName",
|
||||
"onDelete": "CASCADE",
|
||||
"sourceKey": "id",
|
||||
"targetKey": "name",
|
||||
"through": "rolesUsers",
|
||||
"uiSchema": {
|
||||
"type": "array",
|
||||
"title": "{{t(\"Roles\")}}",
|
||||
"x-component": "AssociationField",
|
||||
"x-component-props": {
|
||||
"multiple": true,
|
||||
"fieldNames": {
|
||||
"label": "title",
|
||||
"value": "name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pqnenvqrzxr",
|
||||
"name": "roles",
|
||||
"inherit": false,
|
||||
"hidden": false,
|
||||
"description": null,
|
||||
"category": [],
|
||||
"namespace": "acl.acl",
|
||||
"duplicator": {
|
||||
"dumpable": "required",
|
||||
"with": "uiSchemas"
|
||||
},
|
||||
"autoGenId": false,
|
||||
"model": "RoleModel",
|
||||
"filterTargetKey": "name",
|
||||
"sortable": true,
|
||||
"from": "db2cm",
|
||||
"title": "{{t(\"Roles\")}}",
|
||||
"rawTitle": "{{t(\"Roles\")}}",
|
||||
"fields": [
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Role UID\")}}",
|
||||
"x-component": "Input",
|
||||
"rawTitle": "{{t(\"Role UID\")}}"
|
||||
},
|
||||
"key": "jbz9m80bxmp",
|
||||
"name": "name",
|
||||
"type": "uid",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"prefix": "r_",
|
||||
"primaryKey": true
|
||||
},
|
||||
{
|
||||
"uiSchema": {
|
||||
"type": "string",
|
||||
"title": "{{t(\"Role name\")}}",
|
||||
"x-component": "Input",
|
||||
"rawTitle": "{{t(\"Role name\")}}"
|
||||
},
|
||||
"key": "faywtz4sf3u",
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"interface": "input",
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null,
|
||||
"unique": true,
|
||||
"translation": true
|
||||
},
|
||||
{
|
||||
"key": "1enkovm9sye",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"interface": null,
|
||||
"description": null,
|
||||
"collectionName": "roles",
|
||||
"parentKey": null,
|
||||
"reverseKey": null
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -1,98 +0,0 @@
|
||||
import {
|
||||
Application,
|
||||
ApplicationOptions,
|
||||
CardItem,
|
||||
Plugin,
|
||||
CollectionPlugin,
|
||||
DataBlockProvider,
|
||||
DEFAULT_DATA_SOURCE_KEY,
|
||||
DEFAULT_DATA_SOURCE_TITLE,
|
||||
LocalDataSource,
|
||||
} from '@nocobase/client';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { ComponentType } from 'react';
|
||||
import collections from './collections.json';
|
||||
|
||||
const defaultMocks = {
|
||||
'users:list': {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
username: 'jack',
|
||||
nickname: 'Jack Ma',
|
||||
email: 'test@gmail.com',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
username: 'jim',
|
||||
nickname: 'Jim Green',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
username: 'tom',
|
||||
nickname: 'Tom Cat',
|
||||
email: 'tom@gmail.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
'roles:list': {
|
||||
data: [
|
||||
{
|
||||
name: 'root',
|
||||
title: 'Root',
|
||||
description: 'Root',
|
||||
},
|
||||
{
|
||||
name: 'admin',
|
||||
title: 'Admin',
|
||||
description: 'Admin description',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function createApp(
|
||||
Demo: ComponentType<any>,
|
||||
options: ApplicationOptions = {},
|
||||
mocks: Record<string, any> = defaultMocks,
|
||||
) {
|
||||
class MyPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.dataSourceManager.addDataSource(LocalDataSource, {
|
||||
key: DEFAULT_DATA_SOURCE_KEY,
|
||||
displayName: DEFAULT_DATA_SOURCE_TITLE,
|
||||
collections: collections as any,
|
||||
});
|
||||
}
|
||||
}
|
||||
const app = new Application({
|
||||
apiClient: {
|
||||
baseURL: 'http://localhost:8000',
|
||||
},
|
||||
providers: [Demo],
|
||||
...options,
|
||||
components: {
|
||||
...options.components,
|
||||
DataBlockProvider,
|
||||
CardItem,
|
||||
},
|
||||
plugins: [CollectionPlugin, MyPlugin, ...(options.plugins || [])],
|
||||
designable: true,
|
||||
});
|
||||
|
||||
const mock = new MockAdapter(app.apiClient.axios);
|
||||
|
||||
Object.entries(mocks).forEach(([url, data]) => {
|
||||
mock.onGet(url).reply(async (config) => {
|
||||
const res = typeof data === 'function' ? data(config) : data;
|
||||
return [200, res];
|
||||
});
|
||||
mock.onPost(url).reply(async (config) => {
|
||||
const res = typeof data === 'function' ? data(config) : data;
|
||||
return [200, res];
|
||||
});
|
||||
});
|
||||
|
||||
const Root = app.getRootComponent();
|
||||
return Root;
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
import * as icons from '@ant-design/icons';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { createApp } from './createApp';
|
||||
|
||||
// 自定义模型类,继承自 FlowModel
|
||||
class MyModel extends FlowModel {
|
||||
render() {
|
||||
console.log('Rendering MyModel with props:', this.props);
|
||||
return (
|
||||
<Button
|
||||
{...this.props}
|
||||
onClick={(event) => {
|
||||
this.dispatchEvent('onClick', { event });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const myPropsFlow = defineFlow({
|
||||
key: 'myPropsFlow',
|
||||
auto: true,
|
||||
title: '按钮配置',
|
||||
steps: {
|
||||
setProps: {
|
||||
title: '按钮属性设置',
|
||||
uiSchema: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '按钮标题',
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
title: '类型',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [
|
||||
{ label: '主要', value: 'primary' },
|
||||
{ label: '次要', value: 'default' },
|
||||
{ label: '危险', value: 'danger' },
|
||||
{ label: '虚线', value: 'dashed' },
|
||||
{ label: '链接', value: 'link' },
|
||||
{ label: '文本', value: 'text' },
|
||||
],
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
title: '图标',
|
||||
'x-component': 'Select',
|
||||
'x-decorator': 'FormItem',
|
||||
enum: [
|
||||
{ label: '搜索', value: 'SearchOutlined' },
|
||||
{ label: '添加', value: 'PlusOutlined' },
|
||||
{ label: '删除', value: 'DeleteOutlined' },
|
||||
{ label: '编辑', value: 'EditOutlined' },
|
||||
{ label: '设置', value: 'SettingOutlined' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
type: 'primary',
|
||||
title: 'Primary Button',
|
||||
},
|
||||
// 步骤处理函数,设置模型属性
|
||||
handler(ctx, params) {
|
||||
console.log('Setting props:', params);
|
||||
ctx.model.setProps('children', params.title);
|
||||
ctx.model.setProps('type', params.type);
|
||||
ctx.model.setProps('icon', params.icon ? React.createElement(icons[params.icon]) : undefined);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
MyModel.registerFlow(myPropsFlow);
|
||||
|
||||
class PluginDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ MyModel });
|
||||
const model = this.flowEngine.createModel({
|
||||
use: 'MyModel',
|
||||
});
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <FlowModelRenderer model={model} showFlowSettings />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default createApp({ plugins: [PluginDemo] });
|
@ -0,0 +1,89 @@
|
||||
import * as icons from '@ant-design/icons';
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { defineAction, defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Button } from 'antd';
|
||||
import React from 'react';
|
||||
import { createApp } from './createApp';
|
||||
|
||||
// 自定义模型类,继承自 FlowModel
|
||||
class MyModel extends FlowModel {
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
{...this.props}
|
||||
onClick={(event) => {
|
||||
this.dispatchEvent('click', { event });
|
||||
}}
|
||||
>
|
||||
点击我
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const myEventFlow = defineFlow({
|
||||
key: 'myEventFlow',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
confirm: {
|
||||
use: 'confirm',
|
||||
},
|
||||
next: {
|
||||
handler(ctx) {
|
||||
ctx.globals.message.success(`继续执行后续操作`);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
MyModel.registerFlow(myEventFlow);
|
||||
|
||||
const myConfirm = defineAction({
|
||||
name: 'confirm',
|
||||
uiSchema: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Confirm title',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
title: 'Confirm content',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
title: 'Confirm Deletion',
|
||||
content: 'Are you sure you want to delete this record?',
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
const confirmed = await ctx.globals.modal.confirm({
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
});
|
||||
if (!confirmed) {
|
||||
ctx.globals.message.info('Action cancelled.');
|
||||
return ctx.exit();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
class PluginDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ MyModel });
|
||||
this.flowEngine.registerAction(myConfirm);
|
||||
const model = this.flowEngine.createModel({
|
||||
use: 'MyModel',
|
||||
});
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <FlowModelRenderer model={model} showFlowSettings />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default createApp({ plugins: [PluginDemo] });
|
@ -0,0 +1,9 @@
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
|
||||
export function createApp({ plugins = [] }: { plugins?: Array<typeof Plugin> } = {}) {
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [...plugins],
|
||||
});
|
||||
return app.getRootComponent();
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Button, ConfigProvider, theme } from 'antd';
|
||||
import React from 'react';
|
||||
import { createApp } from './createApp';
|
||||
|
||||
class MyPopupModel extends FlowModel {
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
{...this.props}
|
||||
onClick={(event) => {
|
||||
this.dispatchEvent('onClick', { event });
|
||||
}}
|
||||
>
|
||||
点击我
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const myEventFlow = defineFlow({
|
||||
key: 'myEventFlow',
|
||||
on: {
|
||||
eventName: 'onClick',
|
||||
},
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
ctx.globals.drawer.open({
|
||||
title: '命令式 Drawer',
|
||||
content: (
|
||||
<div>
|
||||
<p>这是命令式打开的 Drawer1 内容</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
ctx.globals.drawer.open({
|
||||
title: '命令式 Drawer',
|
||||
content: (
|
||||
<div>
|
||||
<p>这是命令式打开的 Drawer2 内容</p>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
MyPopupModel.registerFlow(myEventFlow);
|
||||
|
||||
function CustomConfigProvider({ children }) {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.darkAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#52c41a',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
class PluginDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ MyPopupModel });
|
||||
const model = this.flowEngine.createModel({
|
||||
use: 'MyPopupModel',
|
||||
});
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <FlowModelRenderer model={model} showFlowSettings />,
|
||||
});
|
||||
this.app.providers.unshift([CustomConfigProvider, {}]);
|
||||
}
|
||||
}
|
||||
|
||||
export default createApp({ plugins: [PluginDemo] });
|
@ -0,0 +1,38 @@
|
||||
// components/drawer/useDrawer/DrawerComponent.tsx
|
||||
import { Drawer } from 'antd';
|
||||
import React, { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
interface DrawerComponentProps extends React.ComponentProps<typeof Drawer> {
|
||||
afterClose?: () => void;
|
||||
content?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DrawerComponent = forwardRef<unknown, DrawerComponentProps>(({ afterClose, ...props }, ref) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [config, setConfig] = useState(props);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
destroy: () => setVisible(false),
|
||||
update: (newConfig) => setConfig((prev) => ({ ...prev, ...newConfig })),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
{...config}
|
||||
open={visible}
|
||||
onClose={(e) => {
|
||||
setVisible(false);
|
||||
config.onClose?.(e);
|
||||
}}
|
||||
afterOpenChange={(open) => {
|
||||
if (!open) {
|
||||
afterClose?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{config.content}
|
||||
</Drawer>
|
||||
);
|
||||
});
|
||||
|
||||
export default DrawerComponent;
|
@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import DrawerComponent from './DrawerComponent';
|
||||
import usePatchElement from './usePatchElement';
|
||||
|
||||
let uuid = 0;
|
||||
|
||||
function useDrawer() {
|
||||
const holderRef = React.useRef(null);
|
||||
|
||||
const open = (config) => {
|
||||
uuid += 1;
|
||||
const drawerRef = React.createRef<{ destroy: () => void; update: (config: any) => void }>();
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let closeFunc: (() => void) | undefined;
|
||||
const drawer = (
|
||||
<DrawerComponent
|
||||
key={`drawer-${uuid}`}
|
||||
ref={drawerRef}
|
||||
{...config}
|
||||
afterClose={() => {
|
||||
closeFunc?.();
|
||||
config.onClose?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
closeFunc = holderRef.current?.patchElement(drawer);
|
||||
|
||||
return {
|
||||
destroy: () => drawerRef.current?.destroy(),
|
||||
update: (newConfig) => drawerRef.current?.update(newConfig),
|
||||
};
|
||||
};
|
||||
|
||||
const api = React.useMemo(() => ({ open }), []);
|
||||
const ElementsHolder = React.memo(
|
||||
React.forwardRef((props, ref) => {
|
||||
const [elements, patchElement] = usePatchElement();
|
||||
React.useImperativeHandle(ref, () => ({ patchElement }), []);
|
||||
return <>{elements}</>;
|
||||
}),
|
||||
);
|
||||
|
||||
return [api, <ElementsHolder key="drawer-holder" ref={holderRef} />];
|
||||
}
|
||||
|
||||
export default useDrawer;
|
@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export default function usePatchElement(): [React.ReactElement[], (element: React.ReactElement) => () => void] {
|
||||
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
|
||||
|
||||
const patchElement = React.useCallback((element: React.ReactElement) => {
|
||||
// append a new element to elements (and create a new ref)
|
||||
setElements((originElements) => [...originElements, element]);
|
||||
|
||||
// return a function that removes the new element out of elements (and create a new ref)
|
||||
// it works a little like useEffect
|
||||
return () => {
|
||||
setElements((originElements) => originElements.filter((ele) => ele !== element));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return [elements, patchElement];
|
||||
}
|
@ -1 +1,13 @@
|
||||
# Flow Actions
|
||||
|
||||
## 基础示例
|
||||
|
||||
<code src="./demos/basic.tsx"></code>
|
||||
|
||||
## 弹窗
|
||||
|
||||
<code src="./demos/popup.tsx"></code>
|
||||
|
||||
## Confirm
|
||||
|
||||
<code src="./demos/confirm.tsx"></code>
|
||||
|
@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import LazyDropdown, { Item } from './LazyDropdown';
|
||||
|
||||
const items: () => Promise<Item[]> = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500)); // 模拟延迟
|
||||
return [
|
||||
{
|
||||
key: 'dashboard',
|
||||
label: '仪表盘',
|
||||
children: [
|
||||
{ key: 'overview', label: '概览' },
|
||||
{ key: 'analytics', label: '分析' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: '设置',
|
||||
type: 'group',
|
||||
children: async () => {
|
||||
console.log('Loading settings...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟延迟
|
||||
return [
|
||||
{ key: 'profile', label: '个人资料' },
|
||||
{ key: 'preferences', label: '偏好设置' },
|
||||
];
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'reports',
|
||||
label: '报表',
|
||||
children: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟延迟
|
||||
return [
|
||||
{
|
||||
key: 'sales',
|
||||
label: '销售报表',
|
||||
children: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟延迟
|
||||
return [
|
||||
{ key: 'sales-1', label: '销售 子项 1' },
|
||||
{ key: 'sales-2', label: '销售 子项 2' },
|
||||
];
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: '财务报表',
|
||||
children: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
return [
|
||||
{ key: 'finance-1', label: '财务 子项 1' },
|
||||
{ key: 'finance-2', label: '财务 子项 2' },
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
console.log('App rendered');
|
||||
return (
|
||||
<LazyDropdown
|
||||
menu={{
|
||||
onClick(info) {
|
||||
console.log('Menu item clicked:', info);
|
||||
},
|
||||
items,
|
||||
}}
|
||||
>
|
||||
<a>AA</a>
|
||||
</LazyDropdown>
|
||||
);
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
import { Dropdown, DropdownProps, Menu, Spin } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// 菜单项类型定义
|
||||
export type Item = {
|
||||
key?: string;
|
||||
type?: 'group' | 'divider'; // 支持 group 类型
|
||||
label?: React.ReactNode;
|
||||
children?: Item[] | (() => Item[] | Promise<Item[]>);
|
||||
[key: string]: any; // 允许其他属性
|
||||
};
|
||||
|
||||
export type ItemsType = Item[] | (() => Item[] | Promise<Item[]>);
|
||||
|
||||
interface LazyDropdownMenuProps extends Omit<DropdownProps['menu'], 'items'> {
|
||||
items: ItemsType;
|
||||
}
|
||||
|
||||
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
||||
const [loadedChildren, setLoadedChildren] = useState<Record<string, Item[]>>({});
|
||||
const [loadingKeys, setLoadingKeys] = useState<Set<string>>(new Set());
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
||||
const [rootItems, setRootItems] = useState<Item[]>([]);
|
||||
const [rootLoading, setRootLoading] = useState(false);
|
||||
|
||||
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
|
||||
|
||||
// 通用的异步/同步 children 解析
|
||||
const resolveChildren = async (children: Item[] | (() => Item[] | Promise<Item[]>)) => {
|
||||
if (typeof children === 'function') {
|
||||
const res = children();
|
||||
return res instanceof Promise ? await res : res;
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
const handleLoadChildren = async (keyPath: string, loader: () => Item[] | Promise<Item[]>) => {
|
||||
if (loadedChildren[keyPath] || loadingKeys.has(keyPath)) return;
|
||||
|
||||
setLoadingKeys((prev) => new Set(prev).add(keyPath));
|
||||
try {
|
||||
const children = loader();
|
||||
const resolved = children instanceof Promise ? await children : children;
|
||||
setLoadedChildren((prev) => ({ ...prev, [keyPath]: resolved }));
|
||||
} catch (err) {
|
||||
console.error(`Failed to load children for ${keyPath}`, err);
|
||||
} finally {
|
||||
setLoadingKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(keyPath);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 收集所有异步 group
|
||||
const collectAsyncGroups = (items: Item[], path: string[] = []): [string, () => Item[] | Promise<Item[]>][] => {
|
||||
const result: [string, () => Item[] | Promise<Item[]>][] = [];
|
||||
for (const item of items) {
|
||||
const keyPath = getKeyPath(path, item.key);
|
||||
if (item.type === 'group' && typeof item.children === 'function') {
|
||||
result.push([keyPath, item.children]);
|
||||
}
|
||||
if (Array.isArray(item.children)) {
|
||||
result.push(...collectAsyncGroups(item.children, [...path, item.key]));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// 加载根 items,支持同步/异步函数
|
||||
useEffect(() => {
|
||||
const loadRootItems = async () => {
|
||||
let items: Item[];
|
||||
if (typeof menu.items === 'function') {
|
||||
setRootLoading(true);
|
||||
try {
|
||||
const res = menu.items();
|
||||
items = res instanceof Promise ? await res : res;
|
||||
} finally {
|
||||
setRootLoading(false);
|
||||
}
|
||||
} else {
|
||||
items = menu.items;
|
||||
}
|
||||
setRootItems(items);
|
||||
};
|
||||
|
||||
if (menuVisible) {
|
||||
loadRootItems();
|
||||
}
|
||||
}, [menu.items, menuVisible]);
|
||||
|
||||
// 自动加载所有 group 的异步 children
|
||||
useEffect(() => {
|
||||
if (!menuVisible || !rootItems.length) return;
|
||||
const asyncGroups = collectAsyncGroups(rootItems);
|
||||
for (const [keyPath, loader] of asyncGroups) {
|
||||
if (!loadedChildren[keyPath] && !loadingKeys.has(keyPath)) {
|
||||
handleLoadChildren(keyPath, loader);
|
||||
}
|
||||
}
|
||||
}, [menuVisible, rootItems]);
|
||||
|
||||
// 递归解析 items,支持 children 为同步/异步函数
|
||||
const resolveItems = (items: Item[], path: string[] = []): any[] => {
|
||||
return items.map((item) => {
|
||||
const keyPath = getKeyPath(path, item.key);
|
||||
const isGroup = item.type === 'group';
|
||||
const hasAsyncChildren = typeof item.children === 'function';
|
||||
const isLoading = loadingKeys.has(keyPath);
|
||||
const loaded = loadedChildren[keyPath];
|
||||
|
||||
// 非 group 的异步 children,鼠标悬浮时加载
|
||||
const shouldLoadChildren =
|
||||
!isGroup && menuVisible && openKeys.has(keyPath) && hasAsyncChildren && !loaded && !isLoading;
|
||||
|
||||
if (shouldLoadChildren) {
|
||||
handleLoadChildren(keyPath, item.children as () => Item[] | Promise<Item[]>);
|
||||
}
|
||||
|
||||
let children: Item[] | undefined;
|
||||
if (hasAsyncChildren) {
|
||||
children = loaded ?? [];
|
||||
} else if (Array.isArray(item.children)) {
|
||||
children = item.children;
|
||||
}
|
||||
|
||||
if (hasAsyncChildren && !loaded) {
|
||||
children = [
|
||||
{
|
||||
key: `${keyPath}-loading`,
|
||||
label: <Spin size="small" />,
|
||||
disabled: true,
|
||||
} as Item,
|
||||
];
|
||||
}
|
||||
|
||||
if (isGroup) {
|
||||
return {
|
||||
type: 'group',
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
children: children ? resolveItems(children, [...path, item.key]) : [],
|
||||
};
|
||||
}
|
||||
|
||||
if (item.type === 'divider') {
|
||||
return { type: 'divider', key: item.key };
|
||||
}
|
||||
|
||||
return {
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
onClick: (info) => {
|
||||
if (children) {
|
||||
return;
|
||||
}
|
||||
menu.onClick?.({
|
||||
...info,
|
||||
originalItem: item,
|
||||
} as any); // 👈 强制扩展类型
|
||||
},
|
||||
onMouseEnter: () => {
|
||||
setOpenKeys((prev) => {
|
||||
if (prev.has(keyPath)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(keyPath);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
children: children && children.length > 0 ? resolveItems(children, [...path, item.key]) : undefined,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
{...props}
|
||||
dropdownRender={() =>
|
||||
rootLoading && rootItems.length === 0 ? (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: `root-loading`,
|
||||
label: <Spin size="small" />,
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Menu {...menu} onClick={() => {}} items={resolveItems(rootItems)} />
|
||||
)
|
||||
}
|
||||
onOpenChange={(visible) => setMenuVisible(visible)}
|
||||
>
|
||||
{props.children}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default LazyDropdown;
|
@ -0,0 +1,157 @@
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { FlowModel, FlowModelRenderer, AddSubModelButton } from '@nocobase/flow-engine';
|
||||
import { Button, Card } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
class SubModel1 extends FlowModel {
|
||||
render() {
|
||||
return <Card style={{ marginBottom: 24 }}>{this.props.children}</Card>;
|
||||
}
|
||||
}
|
||||
|
||||
SubModel1.registerFlow({
|
||||
key: 'myflow',
|
||||
auto: true,
|
||||
title: '子模型 1',
|
||||
steps: {
|
||||
step1: {
|
||||
title: '步骤 1',
|
||||
paramsRequired: true,
|
||||
uiSchema: {
|
||||
title: {
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '请输入标题',
|
||||
},
|
||||
title: '标题',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps('children', params.title);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
class MyModel extends FlowModel {
|
||||
// 渲染模型内容
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.mapSubModels('items', (item) => (
|
||||
<FlowModelRenderer model={item} showFlowSettings />
|
||||
))}
|
||||
<div />
|
||||
<AddSubModelButton
|
||||
model={this}
|
||||
subModelKey={'items'}
|
||||
items={async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
return [
|
||||
{
|
||||
key: 'subModel1',
|
||||
label: '子模型 1',
|
||||
disabled: true,
|
||||
icon: <span>🔧</span>,
|
||||
createModelOptions: {
|
||||
use: 'SubModel1',
|
||||
stepParams: {
|
||||
myflow: {
|
||||
step1: {
|
||||
title: '子模型 1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'subModel2',
|
||||
label: '子模型 2',
|
||||
icon: <span>🛠️</span>,
|
||||
createModelOptions: {
|
||||
use: 'SubModel1',
|
||||
stepParams: {
|
||||
myflow: {
|
||||
step1: {
|
||||
title: '子模型 2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'b-group',
|
||||
label: '模型 B 组',
|
||||
icon: <span>🛠️</span>,
|
||||
children: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
return [
|
||||
{
|
||||
key: 'b1',
|
||||
label: '模型 B1',
|
||||
icon: <span>🛠️</span>,
|
||||
createModelOptions: {
|
||||
use: 'SubModel1',
|
||||
stepParams: {
|
||||
myflow: {
|
||||
step1: {
|
||||
title: '子模型 B1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'b2',
|
||||
label: '模型 B2',
|
||||
icon: <span>🛠️</span>,
|
||||
createModelOptions: {
|
||||
use: 'SubModel1',
|
||||
stepParams: {
|
||||
myflow: {
|
||||
step1: {
|
||||
title: '子模型 B2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
];
|
||||
}}
|
||||
>
|
||||
<Button>添加子模型</Button>
|
||||
</AddSubModelButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 插件类,负责注册模型、仓库,并加载或创建模型实例
|
||||
class PluginHelloModel extends Plugin {
|
||||
async load() {
|
||||
// 注册自定义模型
|
||||
this.flowEngine.registerModels({ MyModel, SubModel1 });
|
||||
// 加载或创建模型实例(如不存在则创建并初始化)
|
||||
const model = this.flowEngine.createModel({
|
||||
uid: 'my-model',
|
||||
use: 'MyModel',
|
||||
});
|
||||
// 注册路由,渲染模型
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <FlowModelRenderer model={model} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建应用实例,注册插件
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [PluginHelloModel],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -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);
|
||||
|
@ -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`,提升操作安全性和灵活性。
|
||||
|
@ -7,18 +7,34 @@
|
||||
## 核心结构
|
||||
|
||||
```ts
|
||||
interface FlowDefinition {
|
||||
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
|
||||
key: string; // 流唯一标识
|
||||
on?: { event: string }; // 可选:事件触发配置
|
||||
title?: string; // 可选:流显示名称
|
||||
auto?: boolean; // 可选:是否自动运行
|
||||
steps: Record<string, StepDefinition>; // 流步骤定义
|
||||
sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数
|
||||
on?: { eventName: string }; // 可选:事件触发配置
|
||||
steps: Record<string, StepDefinition<TModel>>; // 流步骤定义
|
||||
}
|
||||
|
||||
interface StepDefinition {
|
||||
use?: string; // 可选:引用已注册的全局 Action
|
||||
defaultParams?: any; // 默认参数
|
||||
uiSchema?: any; // 可选:用于 FlowSettings 配置界面
|
||||
handler?: (ctx: any, params: any) => Promise<any>; // 可选:步骤处理函数
|
||||
// 步骤定义支持两种类型:ActionStepDefinition 和 InlineStepDefinition
|
||||
interface ActionStepDefinition<TModel extends FlowModel = FlowModel> {
|
||||
use: string; // 引用已注册的全局 Action 名称
|
||||
title?: string; // 可选:步骤显示名称
|
||||
isAwait?: boolean; // 可选:是否等待步骤执行完成,默认为 true
|
||||
defaultParams?: Record<string, any> | ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>); // 可选:默认参数,支持静态对象或动态函数
|
||||
uiSchema?: Record<string, ISchema>; // 可选:用于 FlowSettings 配置界面
|
||||
paramsRequired?: boolean; // 可选:是否需要参数配置,为 true 时添加模型前会打开配置对话框
|
||||
hideInSettings?: boolean; // 可选:是否在设置菜单中隐藏该步骤
|
||||
}
|
||||
|
||||
interface InlineStepDefinition<TModel extends FlowModel = FlowModel> {
|
||||
handler: (ctx: FlowContext<TModel>, params: any) => Promise<any> | any; // 步骤处理函数
|
||||
title?: string; // 可选:步骤显示名称
|
||||
isAwait?: boolean; // 可选:是否等待步骤执行完成,默认为 true
|
||||
defaultParams?: Record<string, any> | ((ctx: ParamsContext<TModel>) => Record<string, any> | Promise<Record<string, any>>); // 可选:默认参数,支持静态对象或动态函数
|
||||
uiSchema?: Record<string, ISchema>; // 可选:用于 FlowSettings 配置界面
|
||||
paramsRequired?: boolean; // 可选:是否需要参数配置,为 true 时添加模型前会打开配置对话框
|
||||
hideInSettings?: boolean; // 可选:是否在设置菜单中隐藏该步骤
|
||||
}
|
||||
```
|
||||
|
||||
@ -38,20 +54,41 @@ type MyFlowSteps = {
|
||||
|
||||
const myFlow = defineFlow<MyFlowSteps>({
|
||||
key: 'myFlow',
|
||||
on: { event: 'user.created' }, // 监听 user.created 事件自动触发
|
||||
title: '我的流程',
|
||||
auto: true, // 自动执行
|
||||
sort: 100, // 执行顺序
|
||||
on: { eventName: 'user.created' }, // 监听 user.created 事件自动触发
|
||||
steps: {
|
||||
step1: {
|
||||
defaultParams: {},
|
||||
title: '步骤1',
|
||||
// 静态默认参数
|
||||
defaultParams: {
|
||||
name: 'test'
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
// 步骤 1 的处理逻辑
|
||||
ctx.logger.info('执行步骤1', params);
|
||||
// 例如:console.log(params.name);
|
||||
}
|
||||
},
|
||||
step2: {
|
||||
uiSchema: {}, // 可用于 UI 配置
|
||||
defaultParams: {},
|
||||
title: '步骤2',
|
||||
uiSchema: {
|
||||
age: {
|
||||
type: 'number',
|
||||
title: '年龄',
|
||||
'x-component': 'InputNumber',
|
||||
}
|
||||
}, // 可用于 UI 配置
|
||||
// 动态默认参数 - 根据模型状态生成
|
||||
defaultParams: (ctx) => ({
|
||||
name: ctx.model.name,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
async handler(ctx, params) {
|
||||
// 步骤 2 的处理逻辑
|
||||
ctx.logger.info('执行步骤2', params);
|
||||
// 可以访问前一步的结果:ctx.stepResults.step1
|
||||
// 例如:console.log(params.age);
|
||||
}
|
||||
},
|
||||
@ -70,6 +107,9 @@ MyFlowModel.registerFlow(myFlow); // 注册流
|
||||
```ts
|
||||
class MyFlowDefinition implements FlowDefinition {
|
||||
key = 'MyFlowDefinition';
|
||||
title = '我的复杂流程';
|
||||
auto = true;
|
||||
sort = 0;
|
||||
|
||||
steps = {
|
||||
step1: {
|
||||
@ -156,8 +196,8 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' });
|
||||
|
||||
```ts
|
||||
await myModel.applyFlow('myFlow'); // 主动执行指定流
|
||||
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.event)
|
||||
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
||||
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName)
|
||||
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序
|
||||
```
|
||||
|
||||
---
|
||||
@ -169,8 +209,10 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ----------- | -------------------------------- | ---------------------------------- |
|
||||
| `key` | `string` | 流唯一标识,必须配置 |
|
||||
| `on` | `{ event: string }` | (可选)事件触发配置 |
|
||||
| `title` | `string` | (可选)流显示名称 |
|
||||
| `on` | `{ eventName: string }` | (可选)事件触发配置 |
|
||||
| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 |
|
||||
| `sort` | `number` | (可选)流执行排序,数字越小越先执行,默认为 0,可为负数 |
|
||||
| `steps` | `Record<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
|
||||
|
||||
### StepDefinition 配置速查表
|
||||
@ -178,8 +220,12 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | -------------------------------------- | ------------------------------------- |
|
||||
| `use` | `string` | (可选)引用已注册的全局 Action |
|
||||
| `defaultParams` | `any` | 步骤的默认参数 |
|
||||
| `title` | `string` | (可选)步骤显示名称 |
|
||||
| `isAwait` | `boolean` | (可选)是否等待步骤执行完成,默认为 true |
|
||||
| `defaultParams` | `Record<string, any>` \| `(ctx: ParamsContext) => Record<string, any> \| Promise<Record<string, any>>` | (可选)步骤的默认参数,支持静态对象或动态函数 |
|
||||
| `uiSchema` | `any` | (可选)用于 FlowSettings UI 渲染 |
|
||||
| `paramsRequired`| `boolean` | (可选)是否需要参数配置,为 true 时添加模型前会打开配置对话框 |
|
||||
| `hideInSettings`| `boolean` | (可选)是否在设置菜单中隐藏该步骤 |
|
||||
| `handler` | `(ctx, params) => Promise<any>` | (可选)步骤执行逻辑,若未定义则使用 `use` 指定的全局 Action |
|
||||
|
||||
---
|
||||
|
@ -82,6 +82,116 @@
|
||||
- **flowSettings.openStepSettingsDialog(props: StepSettingsDialogProps)**
|
||||
显示单个步骤的配置界面。
|
||||
|
||||
- **flowSettings.openRequiredParamsStepFormDialog(props: StepFormDialogProps)**
|
||||
显示多个需要配置参数的步骤的分步表单界面。
|
||||
|
||||
#### 工具栏扩展 (Toolbar Extensions)
|
||||
|
||||
FlowSettings 支持在右上角悬浮工具栏中添加自定义项目组件:
|
||||
|
||||
- **flowSettings.addToolbarItem(config: ToolbarItemConfig): void**
|
||||
添加单个工具栏项目。
|
||||
|
||||
- **flowSettings.addToolbarItems(configs: ToolbarItemConfig[]): void**
|
||||
批量添加工具栏项目。
|
||||
|
||||
- **flowSettings.removeToolbarItem(key: string): void**
|
||||
移除指定的工具栏项目。
|
||||
|
||||
- **flowSettings.getToolbarItems(): ToolbarItemConfig[]**
|
||||
获取所有工具栏项目。
|
||||
|
||||
- **flowSettings.clearToolbarItems(): void**
|
||||
清空所有工具栏项目。
|
||||
|
||||
**ToolbarItemConfig 接口:**
|
||||
```typescript
|
||||
interface ToolbarItemConfig {
|
||||
key: string; // 项目唯一标识
|
||||
component: React.ComponentType<{ model: FlowModel }>; // 项目组件
|
||||
visible?: (model: FlowModel) => boolean; // 显示条件函数
|
||||
sort?: number; // 排序权重,数字越小或越晚添加的越靠右
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
```typescript
|
||||
import { useFlowEngine } from '@nocobase/flow-engine';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { Tooltip, message } from 'antd';
|
||||
|
||||
const CopyIcon: React.FC<{ model: FlowModel }> = ({ model }) => {
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(model.uid);
|
||||
message.success('UID 已复制');
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip title="复制 UID">
|
||||
<CopyOutlined
|
||||
onClick={handleCopy}
|
||||
style={{ cursor: 'pointer', fontSize: 12 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
// 注册工具栏项目
|
||||
const MyComponent = () => {
|
||||
const flowEngine = useFlowEngine();
|
||||
|
||||
useEffect(() => {
|
||||
flowEngine.flowSettings.addToolbarItem({
|
||||
key: 'copy',
|
||||
component: CopyIcon,
|
||||
sort: 10
|
||||
});
|
||||
}, [flowEngine]);
|
||||
|
||||
return <div>My Component</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- 工具栏项目组件内部需要处理所有逻辑(点击、菜单、状态等)
|
||||
- 使用 Tooltip 提供操作说明,提升用户体验
|
||||
|
||||
#### 步骤上下文 (Step Context)
|
||||
|
||||
FlowSettings 为配置组件提供了上下文功能,使组件能够访问当前步骤的相关信息:
|
||||
|
||||
- **useStepSettingContext(): StepSettingContextType**
|
||||
React Hook,用于在配置组件中获取当前步骤的上下文信息,包括:
|
||||
- `model`: 当前的 FlowModel 实例
|
||||
- `globals`: 全局上下文数据
|
||||
- `app`: FlowEngine 应用实例
|
||||
- `step`: 当前步骤定义
|
||||
- `flow`: 当前流程定义
|
||||
- `flowKey`: 流程标识
|
||||
- `stepKey`: 步骤标识
|
||||
|
||||
**使用示例:**
|
||||
```typescript
|
||||
import { useStepSettingContext } from '@nocobase/flow-engine';
|
||||
|
||||
const MyCustomSettingField = () => {
|
||||
const { model, step, flow, flowKey, stepKey } = useStepSettingContext();
|
||||
|
||||
// 基于当前步骤信息进行自定义逻辑
|
||||
const handleAction = () => {
|
||||
console.log('当前步骤:', step.title);
|
||||
console.log('所属流程:', flow.title);
|
||||
};
|
||||
|
||||
return <Input />;
|
||||
};
|
||||
```
|
||||
|
||||
**注意:**
|
||||
- 在单步骤配置对话框中,上下文提供完整的步骤信息
|
||||
- 在多步骤表单中,上下文会随着步骤切换动态更新
|
||||
- 上下文同时也会添加到 SchemaField 的 scope 中,可在 uiSchema 中直接使用
|
||||
|
||||
---
|
||||
|
||||
## 示例
|
||||
|
@ -1,2 +1,55 @@
|
||||
# FlowHooks
|
||||
|
||||
Flow Engine 提供了一系列 React Hooks 来简化 FlowModel 的使用和流程执行。
|
||||
|
||||
## Model Hooks
|
||||
|
||||
### useFlowModel
|
||||
|
||||
从 React Context 中获取 FlowModel 实例,避免 prop drilling。
|
||||
|
||||
```tsx
|
||||
import { FlowModelProvider, useFlowModel } from '@nocobase/flow-engine';
|
||||
|
||||
// 提供 model 上下文
|
||||
<FlowModelProvider model={model}>
|
||||
<ChildComponent />
|
||||
</FlowModelProvider>
|
||||
|
||||
// 在子组件中获取 model
|
||||
const ChildComponent = () => {
|
||||
const model = useFlowModel<MyFlowModel>();
|
||||
return <div>{model.uid}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**参数**:无
|
||||
**返回值**:`T extends FlowModel` - FlowModel 实例
|
||||
**异常**:如果在 FlowModelProvider 外部使用会抛出错误
|
||||
|
||||
### useFlowModelById
|
||||
|
||||
根据 UID 获取或创建 FlowModel 实例。
|
||||
|
||||
```tsx
|
||||
import { useFlowModelById } from '@nocobase/flow-engine';
|
||||
|
||||
const MyComponent = () => {
|
||||
const model = useFlowModelById<MyFlowModel>(
|
||||
'my-model-uid',
|
||||
'MyFlowModel',
|
||||
{ defaultFlow: { step1: { name: 'test' } } }
|
||||
);
|
||||
|
||||
return <div>{model.props.name}</div>;
|
||||
};
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `uid: string` - 模型唯一标识
|
||||
- `modelClassName?: string` - 模型类名(用于创建新实例)
|
||||
- `stepParams?: StepParams` - 初始步骤参数
|
||||
|
||||
**返回值**:`T extends FlowModel` - FlowModel 实例
|
||||
|
||||
## 流程执行 Hooks
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -110,6 +110,9 @@
|
||||
- **addSubModel(subKey: string, options): FlowModel**
|
||||
创建并添加一个子模型到数组字段(如 tabs、columns)。
|
||||
|
||||
- **findSubModel\<K, R\>(subKey: K, callback: (model) => R): R**
|
||||
查找子模型
|
||||
|
||||
- **mapSubModels\<K, R\>(subKey: K, callback: (model) => R): R[]**
|
||||
遍历指定 key 的子模型,对每个子模型执行 callback 函数,并返回结果数组。
|
||||
- 支持完整的类型推导,callback 参数会自动推导为正确的模型类型
|
||||
@ -207,127 +210,3 @@ interface DefaultStructure {
|
||||
subModels?: Record<string, FlowModel | FlowModel[]>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 子模型添加按钮组件
|
||||
|
||||
为方便在界面中动态添加子模型,框架提供了 4 个 React 按钮组件:
|
||||
|
||||
1. `AddSubModelButton`(通用)
|
||||
2. `AddBlockButton`(添加区块模型)
|
||||
3. `AddFieldButton`(添加字段模型)
|
||||
4. `AddActionButton`(添加 Action 模型)
|
||||
|
||||
### 通用添加子模型
|
||||
|
||||
`AddSubModelButton` 是最基础的按钮组件,用于向任意父模型添加任意类型的子模型。其余三个按钮组件都基于它做了场景化封装。
|
||||
|
||||
#### 主要 Props
|
||||
|
||||
| Prop | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | `FlowModel` **(必填)** | 当前父模型实例 |
|
||||
| `items` | `AddSubModelMenuItem[]` **(必填)** | 可供选择的子模型类型列表 |
|
||||
| `subModelType` | `'object' \| 'array'` | 指定子模型是对象字段还是数组字段,默认为 `'array'` |
|
||||
| `subModelKey` | `string` | 子模型在父模型中的字段名 |
|
||||
| `ParentModelClass` | `string \| ModelConstructor` | 父模型类名(用于过滤支持的子模型类型) |
|
||||
| `onModelAdded` | `(subModel, item) => Promise<void>` | 添加成功后的回调,可返回 Promise 以执行异步逻辑 |
|
||||
| `children` | `ReactNode` | 按钮文案,默认 `"Add"` |
|
||||
| `buildSubModelParams` | `(item) => CreateModelOptions \| FlowModel` | 自定义子模型创建参数 |
|
||||
|
||||
#### 菜单项定义 `AddSubModelMenuItem`
|
||||
|
||||
```ts
|
||||
interface AddSubModelMenuItem {
|
||||
key: string; // 唯一键
|
||||
label: string; // 菜单展示文案
|
||||
icon?: ReactNode; // 可选图标
|
||||
item: typeof FlowModel; // 对应的模型类
|
||||
use: string; // createModel 时的 use 值
|
||||
}
|
||||
```
|
||||
|
||||
#### 组件行为
|
||||
|
||||
以 **鼠标悬停** 方式展示下拉菜单,用户点击菜单项后执行相应的子模型添加逻辑。
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```ts
|
||||
<AddSubModelButton
|
||||
model={parentModel}
|
||||
subModelKey="tabs"
|
||||
subModelType="array"
|
||||
items={[
|
||||
{
|
||||
key: 'TabFlowModel',
|
||||
label: '选项卡',
|
||||
item: TabFlowModel,
|
||||
use: 'TabFlowModel',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### 添加区块子模型
|
||||
|
||||
`AddBlockButton` 专门用于向父模型添加**区块子模型**。相比 `AddSubModelButton`,它会自动根据 `ParentModelClass` 检索所有合法的区块模型类并构造菜单,不需要手动传入 `items`。
|
||||
|
||||
#### 额外 Props
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `ParentModelClass` | `string` | `'BlockFlowModel'` | 区块模型的父类名 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```ts
|
||||
<AddBlockButton
|
||||
model={gridModel}
|
||||
// 其余参数均可使用默认值
|
||||
/>
|
||||
```
|
||||
|
||||
### 添加字段子模型
|
||||
|
||||
`AddFieldButton` 用于为 **字段** 相关的父模型(如表格列、表单项)快速添加对应的字段子模型。会自动根据 `collection` 中的所有 CollectionField 自动匹配合适的模型类并构造菜单,不需要手动传入 `items`。
|
||||
|
||||
#### 额外 Props
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `collection` | `Collection` **(必填)** | 字段所属的数据表集合 |
|
||||
| `ParentModelClass` | `string` | `'FieldFlowModel'` | 字段模型的父类名 |
|
||||
| `buildSubModelParams` | `(item) => CreateModelOptions \| FlowModel` | 自定义创建逻辑 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```ts
|
||||
<AddFieldButton
|
||||
model={tableColumnModel}
|
||||
collection={postCollection}
|
||||
ParentModelClass={CollectionFieldFlowModel}
|
||||
buildSubModelParams={buildColumnSubModelParams}
|
||||
onModelAdded={onModelAdded}
|
||||
/>
|
||||
```
|
||||
|
||||
### 添加 Action 子模型
|
||||
|
||||
`AddActionButton` 用于向父模型添加**Action 子模型**。会自动根据 `ParentModelClass` 检索所有合法的 Action 模型类并构造菜单,不需要手动传入 `items`。
|
||||
|
||||
#### 额外 Props
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `ParentModelClass` | `string` | `'ActionFlowModel'` | 动作模型的父类名 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```ts
|
||||
<AddActionButton
|
||||
model={blockModel}
|
||||
ParentModelClass={ActionFlowModel}
|
||||
/>
|
||||
```
|
||||
|
@ -111,6 +111,7 @@ console.log(apiResource.getData());
|
||||
- `setSourceId(sourceId) / getSourceId()`: 设置/获取源对象 ID。
|
||||
- `setDataSourceKey(dataSourceKey) / getDataSourceKey()`: 设置/获取数据源标识(通过 header)。
|
||||
- `setFilter(filter) / getFilter()`: 设置/获取过滤条件。
|
||||
- `addFilterGroup(key, filter) / removeFilterGroup(key)`: 设置/移除条件组。
|
||||
- `setAppends(appends) / getAppends()`: 设置/获取附加字段。
|
||||
- `addAppends(appends) / removeAppends(appends)`: 添加/移除附加字段。
|
||||
- `setFilterByTk(filterByTk) / getFilterByTk()`: 设置/获取主键过滤条件。
|
||||
|
@ -41,9 +41,16 @@ FlowModel 提供了丰富的 API 用于子模型的创建、添加、遍历和
|
||||
| `setParent(parent)` | 设置父模型 |
|
||||
| `createRootModel(options)` | 创建根模型(通常由 flowEngine 调用) |
|
||||
|
||||
**注意事项**
|
||||
|
||||
- 推荐通过 `setSubModel` 和 `addSubModel` 方法管理子模型,避免直接操作 `subModels` 字段。
|
||||
- 子模型字段不存在时会自动初始化为合适的类型(对象或数组)。
|
||||
- 子模型的类型和结构建议通过泛型参数进行类型约束,提升类型安全和开发体验。
|
||||
- 组件添加子模型时,通常会自动维护父子关系和数据同步。
|
||||
|
||||
---
|
||||
|
||||
## 典型用法示例
|
||||
## 用法示例
|
||||
|
||||
```ts
|
||||
// 创建根模型
|
||||
@ -67,14 +74,14 @@ model.mapSubModels('tabs', (tab) => {
|
||||
## 子模型的父子关系
|
||||
|
||||
- 每个子模型都自动维护对父模型的引用(`parent`)。
|
||||
- 父模型通过 `subModels` 字段管理所有子模型。
|
||||
- 父模型通过 `subModels` 管理所有子模型。
|
||||
- 通过 `setParent` 方法可手动设置父模型,但一般无需手动操作。
|
||||
|
||||
---
|
||||
|
||||
## 子模型的使用场景
|
||||
|
||||
作为组件渲染
|
||||
### 作为组件渲染
|
||||
|
||||
```tsx | pure
|
||||
model.mapSubModels('tabs', (tab) => {
|
||||
@ -82,7 +89,7 @@ model.mapSubModels('tabs', (tab) => {
|
||||
});
|
||||
```
|
||||
|
||||
作为属性值使用
|
||||
### 作为属性值使用
|
||||
|
||||
```tsx | pure
|
||||
await model.applySubModelsAutoFlows(ctx);
|
||||
@ -92,150 +99,19 @@ const columns = model.mapSubModels('columns', (column) => column.getProps());
|
||||
|
||||
---
|
||||
|
||||
## 子模型操作相关组件
|
||||
## 子模型管理组件
|
||||
|
||||
NocoBase 提供了多种 React 组件,方便在界面上动态添加、删除子模型,提升开发体验:
|
||||
NocoBase 提供了子模型管理组件,方便在界面上动态添加、删除子模型,提升开发体验:
|
||||
|
||||
### 1. AddSubModelButton(通用添加按钮)
|
||||
- AddSubModelButton
|
||||
- AddBlockModelButton
|
||||
- AddFieldModelButton
|
||||
- AddActionModelButton
|
||||
|
||||
- 用于向任意父模型添加任意类型的子模型。
|
||||
- 支持自定义菜单项、回调、按钮内容等。
|
||||
- 适用于绝大多数子模型添加场景。
|
||||
|
||||
**主要 Props:**
|
||||
|
||||
| Prop | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `model` | `FlowModel` **(必填)** | 当前父模型实例 |
|
||||
| `items` | `AddSubModelMenuItem[]` **(必填)** | 可供选择的子模型类型列表 |
|
||||
| `subModelType` | `'object' \| 'array'` | 指定子模型是对象字段还是数组字段,默认为 `'array'` |
|
||||
| `subModelKey` | `string` | 子模型在父模型中的字段名 |
|
||||
| `ParentModelClass` | `string \| ModelConstructor` | 父模型类名(用于过滤支持的子模型类型) |
|
||||
| `onModelAdded` | `(subModel, item) => Promise<void>` | 添加成功后的回调,可返回 Promise 以执行异步逻辑 |
|
||||
| `children` | `ReactNode` | 按钮文案,默认 `"Add"` |
|
||||
| `buildSubModelParams` | `(item) => CreateModelOptions \| FlowModel` | 自定义子模型创建参数 |
|
||||
|
||||
**菜单项定义:**
|
||||
|
||||
```ts
|
||||
interface AddSubModelMenuItem {
|
||||
key: string; // 唯一键
|
||||
label: string; // 菜单展示文案
|
||||
icon?: ReactNode; // 可选图标
|
||||
item: typeof FlowModel; // 对应的模型类
|
||||
use: string; // createModel 时的 use 值
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```tsx | pure
|
||||
const currentModel = new MyModel();
|
||||
|
||||
<AddSubModelButton
|
||||
model={currentModel}
|
||||
subModelKey="tabs"
|
||||
subModelType="array"
|
||||
items={[
|
||||
{
|
||||
key: 'key1',
|
||||
icon: <Icon />,
|
||||
label: '子模型1',
|
||||
options: {
|
||||
use: 'TabModel',
|
||||
stepParams: {},
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
// 等价于
|
||||
currentModel.addSubModel('tabs', {
|
||||
use: 'TabModel',
|
||||
stepParams: {},
|
||||
});
|
||||
```
|
||||
<code src="./demos/flow-sub-model.tsx"></code>
|
||||
|
||||
---
|
||||
|
||||
### 2. AddBlockButton(添加区块子模型)
|
||||
|
||||
- 专用于向父模型添加**区块子模型**。
|
||||
- 自动根据 `ParentModelClass` 检索所有合法的区块模型类并构造菜单,无需手动传入 `items`。
|
||||
|
||||
**额外 Props:**
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `ParentModelClass` | `string` | `'BlockFlowModel'` | 区块模型的父类名 |
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```tsx | pure
|
||||
<AddBlockButton
|
||||
model={gridModel}
|
||||
// 其余参数均可使用默认值
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. AddFieldButton(添加字段子模型)
|
||||
|
||||
- 用于为**字段相关父模型**(如表格列、表单项)快速添加字段子模型。
|
||||
- 自动根据 `collection` 匹配合适的模型类,无需手动传入 `items`。
|
||||
|
||||
**额外 Props:**
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `collection` | `Collection` **(必填)** | 字段所属的数据表集合 |
|
||||
| `ParentModelClass` | `string` | `'FieldFlowModel'` | 字段模型的父类名 |
|
||||
| `buildSubModelParams` | `(item) => CreateModelOptions \| FlowModel` | 自定义创建逻辑 |
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```tsx | pure
|
||||
<AddFieldButton
|
||||
model={tableColumnModel}
|
||||
collection={postCollection}
|
||||
ParentModelClass={CollectionFieldFlowModel}
|
||||
buildSubModelParams={buildColumnSubModelParams}
|
||||
onModelAdded={onModelAdded}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. AddActionButton(添加 Action 子模型)
|
||||
|
||||
- 用于向父模型添加**Action 子模型**。
|
||||
- 自动根据 `ParentModelClass` 检索所有合法的 Action 模型类并构造菜单,无需手动传入 `items`。
|
||||
|
||||
**额外 Props:**
|
||||
|
||||
| Prop | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `ParentModelClass` | `string` | `'ActionFlowModel'` | 动作模型的父类名 |
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```tsx | pure
|
||||
<AddActionButton
|
||||
model={blockModel}
|
||||
ParentModelClass={ActionFlowModel}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 推荐通过 `setSubModel` 和 `addSubModel` 方法管理子模型,避免直接操作 `subModels` 字段。
|
||||
- 子模型字段不存在时会自动初始化为合适的类型(对象或数组)。
|
||||
- 子模型的类型和结构建议通过泛型参数进行类型约束,提升类型安全和开发体验。
|
||||
- 组件添加子模型时,通常会自动维护父子关系和数据同步。
|
||||
|
||||
---
|
||||
## 总结
|
||||
|
||||
通过子模型机制与配套组件,NocoBase 支持灵活的模型树结构和动态 UI 组织,是低代码建模和流程引擎的基础能力之一。
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { Collection, DataSource, DataSourceManager, Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import {
|
||||
Collection,
|
||||
CollectionField,
|
||||
DataSource,
|
||||
DataSourceManager,
|
||||
FlowModel,
|
||||
FlowModelRenderer,
|
||||
} from '@nocobase/flow-engine';
|
||||
import { Button, Dropdown, Input } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
const dsm = new DataSourceManager();
|
||||
const ds = new DataSource({
|
||||
name: 'main',
|
||||
key: 'main',
|
||||
displayName: 'Main',
|
||||
description: 'This is the main data source',
|
||||
});
|
||||
@ -47,7 +54,7 @@ ds.addCollection({
|
||||
});
|
||||
|
||||
class FieldModel extends FlowModel {
|
||||
field: Field;
|
||||
field: CollectionField;
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
@ -94,7 +101,7 @@ class ConfigureFieldsFlowModel extends FlowModel<S> {
|
||||
getFieldMenuItems() {
|
||||
return this.collection.mapFields((field) => {
|
||||
return {
|
||||
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
|
||||
key: `${this.collection.dataSource.key}.${this.collection.name}.${field.name}`,
|
||||
label: field.title,
|
||||
};
|
||||
});
|
||||
|
@ -70,7 +70,7 @@ class ConfigureFieldsFlowModel extends FlowModel {
|
||||
<Button
|
||||
onClick={() => {
|
||||
dsm.addDataSource({
|
||||
name: `ds-${uid()}`,
|
||||
key: `ds-${uid()}`,
|
||||
displayName: `ds-${uid()}`,
|
||||
});
|
||||
}}
|
||||
|
@ -0,0 +1,174 @@
|
||||
import { Input, Select } from '@formily/antd-v5';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { FlowModel, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine';
|
||||
import { Card, Space, Button } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
class SimpleProductModel extends FlowModel {
|
||||
render() {
|
||||
const { name = '新产品', category = 'electronics', price = 0 } = this.props;
|
||||
|
||||
return (
|
||||
<Card title={name} style={{ width: 250, margin: 8 }}>
|
||||
<p>
|
||||
<strong>分类:</strong> {category}
|
||||
</p>
|
||||
<p>
|
||||
<strong>价格:</strong> ¥{price}
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 注册简单的配置流程
|
||||
SimpleProductModel.registerFlow('configFlow', {
|
||||
title: '产品配置',
|
||||
auto: true,
|
||||
steps: {
|
||||
// 第一步:设置产品名称和分类
|
||||
basicInfo: {
|
||||
title: '基础信息',
|
||||
uiSchema: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: '产品名称',
|
||||
'x-component': Input,
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
title: '分类',
|
||||
'x-component': Select,
|
||||
enum: [
|
||||
{ label: '电子产品', value: 'electronics' },
|
||||
{ label: '服装', value: 'fashion' },
|
||||
{ label: '图书', value: 'books' },
|
||||
],
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
name: '新产品',
|
||||
category: 'electronics',
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps({
|
||||
name: params.name,
|
||||
category: params.category,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
// 第二步:设置价格 - 使用动态 defaultParams
|
||||
priceConfig: {
|
||||
title: '价格设置',
|
||||
uiSchema: {
|
||||
price: {
|
||||
type: 'number',
|
||||
title: '价格',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
// 🔥 关键:动态 defaultParams - 根据分类自动设置默认价格
|
||||
defaultParams: (ctx) => {
|
||||
const category = ctx.model.getProps().category || 'electronics';
|
||||
const priceMap = {
|
||||
electronics: 999, // 电子产品默认999元
|
||||
fashion: 299, // 服装默认299元
|
||||
books: 49, // 图书默认49元
|
||||
};
|
||||
return {
|
||||
price: priceMap[category] || 199,
|
||||
};
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps('price', params.price);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
class PluginDynamicDefaultParams extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ SimpleProductModel });
|
||||
|
||||
// 创建一个简单的产品模型
|
||||
const model = this.flowEngine.createModel({
|
||||
uid: 'simple-product',
|
||||
use: 'SimpleProductModel',
|
||||
props: { name: '示例产品', category: 'electronics', price: 0 },
|
||||
});
|
||||
|
||||
await model.applyAutoFlows();
|
||||
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h2>动态 defaultParams 演示</h2>
|
||||
<p>
|
||||
这个示例展示了 defaultParams 的动态功能:
|
||||
<br />
|
||||
1. 先设置产品分类
|
||||
<br />
|
||||
2. 再配置价格时,会根据分类自动设置不同的默认价格
|
||||
</p>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<FlowsFloatContextMenu model={model}>
|
||||
<FlowModelRenderer model={model} />
|
||||
</FlowsFloatContextMenu>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Space direction="vertical">
|
||||
<Button
|
||||
onClick={() => {
|
||||
model.setStepParams('configFlow', 'basicInfo', {
|
||||
name: '智能手机',
|
||||
category: 'electronics',
|
||||
});
|
||||
model.applyFlow('configFlow');
|
||||
}}
|
||||
>
|
||||
设置为电子产品 (默认价格999元)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
model.setStepParams('configFlow', 'basicInfo', {
|
||||
name: '时尚T恤',
|
||||
category: 'fashion',
|
||||
});
|
||||
model.applyFlow('configFlow');
|
||||
}}
|
||||
>
|
||||
设置为服装 (默认价格299元)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
model.setStepParams('configFlow', 'basicInfo', {
|
||||
name: '编程指南',
|
||||
category: 'books',
|
||||
});
|
||||
model.applyFlow('configFlow');
|
||||
}}
|
||||
>
|
||||
设置为图书 (默认价格49元)
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建应用实例
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [PluginDynamicDefaultParams],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -0,0 +1,83 @@
|
||||
import { Input } from '@formily/antd-v5';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Card, Space, Button } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* 演示 ForkFlowModel:
|
||||
* 1. master 模型 uid = 'shared-uid',维护共享 stepParams
|
||||
* 2. fork1、fork2 具有各自局部 props.title,但点击按钮修改共享 name 后,两处即时同步
|
||||
*/
|
||||
class HelloFlowModel extends FlowModel {
|
||||
render() {
|
||||
const { name, color } = this.getProps();
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ marginTop: 8 }}>name: {name}</div>
|
||||
<div style={{ marginTop: 8, color: color || '#333' }}>color: {color}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 一个简单的 flow,用来把 stepParams.name 写入 props.name
|
||||
HelloFlowModel.registerFlow('setNameFlow', {
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
uiSchema: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Name',
|
||||
'x-component': Input,
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
name: 'NocoBase',
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps('name', params.name);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
class PluginForkDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ HelloFlowModel });
|
||||
|
||||
// 创建 master
|
||||
const master = this.flowEngine.createModel({
|
||||
uid: 'shared-uid',
|
||||
use: 'HelloFlowModel',
|
||||
stepParams: {
|
||||
setNameFlow: {
|
||||
step1: { name: 'NocoBase' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 创建两个 fork
|
||||
const fork1 = master.createFork({ title: 'Fork A', color: 'red' });
|
||||
const fork2 = master.createFork({ title: 'Fork B', color: 'blue' });
|
||||
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: (
|
||||
<Space>
|
||||
<FlowModelRenderer model={master} showFlowSettings />
|
||||
<FlowModelRenderer model={fork1} />
|
||||
<FlowModelRenderer model={fork2} />
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [PluginForkDemo],
|
||||
});
|
||||
|
||||
export default app.getRootComponent();
|
@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { useFlowModel, FlowContext, FlowModel, withFlowModel, FlowsSettings } from '@nocobase/flow-engine';
|
||||
import { useFlowModelById, FlowContext, FlowModel, withFlowModel, FlowsSettings } from '@nocobase/flow-engine';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import Handlebars from 'handlebars';
|
||||
|
||||
const Demo = () => {
|
||||
const uid = 'markdown-block';
|
||||
const model = useFlowModel<FlowModel>(uid, 'MarkdownModel');
|
||||
const model = useFlowModelById<FlowModel>(uid, 'MarkdownModel');
|
||||
return (
|
||||
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
|
||||
<MarkdownBlock model={model} />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as icons from '@ant-design/icons';
|
||||
import { FormItem, Input, Select } from '@formily/antd-v5';
|
||||
import { FormItem, Input, NumberPicker, Select } from '@formily/antd-v5';
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Button, Modal } from 'antd';
|
||||
@ -83,6 +83,20 @@ const myEventFlow = defineFlow({
|
||||
},
|
||||
title: '按钮事件',
|
||||
steps: {
|
||||
modalWidth: {
|
||||
title: '弹窗宽度配置',
|
||||
uiSchema: {
|
||||
width: {
|
||||
type: 'string',
|
||||
title: '弹窗宽度',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'NumberPicker',
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
return params.width || 520;
|
||||
},
|
||||
},
|
||||
confirm: {
|
||||
title: '确认操作配置',
|
||||
uiSchema: {
|
||||
@ -105,6 +119,7 @@ const myEventFlow = defineFlow({
|
||||
},
|
||||
handler(ctx, params) {
|
||||
Modal.confirm({
|
||||
width: ctx.stepResults.modalWidth,
|
||||
...params,
|
||||
});
|
||||
},
|
||||
|
@ -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} /> });
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { FlowModel } from '@nocobase/flow-engine';
|
||||
import { Modal } from 'antd';
|
||||
import { Button, Modal } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export class ActionModel extends FlowModel {
|
||||
@ -8,7 +8,23 @@ export class ActionModel extends FlowModel {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a {...this.props}>{this.props.title || 'Untitle'}</a>;
|
||||
return <Button {...this.props}>{this.props.title || 'Untitle'}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkActionModel extends ActionModel {
|
||||
render() {
|
||||
return (
|
||||
<Button type="link" {...this.props}>
|
||||
{this.props.title || 'View'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteActionModel extends ActionModel {
|
||||
render() {
|
||||
return <Button {...this.props}>{this.props.title || 'Delete'}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,12 +33,24 @@ ActionModel.registerFlow({
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
uiSchema: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '标题',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: '请输入标题',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps('title', params.title);
|
||||
ctx.model.onClick = (e) => {
|
||||
ctx.model.dispatchEvent('click', {
|
||||
event: e,
|
||||
record: ctx.extra.record,
|
||||
...ctx.extra,
|
||||
});
|
||||
};
|
||||
},
|
||||
@ -30,7 +58,7 @@ ActionModel.registerFlow({
|
||||
},
|
||||
});
|
||||
|
||||
ActionModel.registerFlow({
|
||||
LinkActionModel.registerFlow({
|
||||
key: 'event1',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
@ -38,7 +66,7 @@ ActionModel.registerFlow({
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
Modal.confirm({
|
||||
ctx.globals.modal.confirm({
|
||||
title: `${ctx.extra.record?.id}`,
|
||||
content: 'Are you sure you want to perform this action?',
|
||||
onOk: async () => {},
|
||||
@ -47,3 +75,21 @@ ActionModel.registerFlow({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
DeleteActionModel.registerFlow({
|
||||
key: 'event1',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
ctx.globals.modal.confirm({
|
||||
title: `Selected Rows`,
|
||||
content: <pre>{JSON.stringify(ctx.extra.currentResource?.getSelectedRows(), null, 2)}</pre>,
|
||||
// onOk: async () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { DataSource, DataSourceManager } from '@nocobase/flow-engine';
|
||||
export const dsm = new DataSourceManager();
|
||||
|
||||
const ds = new DataSource({
|
||||
name: 'main',
|
||||
key: 'main',
|
||||
displayName: 'Main',
|
||||
description: 'This is the main data source',
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Plugin } from '@nocobase/client';
|
||||
import { FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import actions from 'packages/plugins/@nocobase/plugin-workflow/src/server/actions';
|
||||
import React from 'react';
|
||||
import { createApp } from '../createApp';
|
||||
import { FormItemModel } from '../form/form-item-model';
|
||||
import { FormModel } from '../form/form-model';
|
||||
import { SubmitActionModel } from '../form/submit-action-model';
|
||||
import { ActionModel } from './action-model';
|
||||
import { ActionModel, DeleteActionModel, LinkActionModel } from './action-model';
|
||||
import { dsm } from './data-source-manager';
|
||||
import { TableColumnActionsModel, TableColumnModel } from './table-column-model';
|
||||
import { TableModel } from './table-model';
|
||||
@ -14,6 +15,8 @@ class PluginDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.context.dsm = dsm;
|
||||
this.flowEngine.registerModels({
|
||||
DeleteActionModel,
|
||||
LinkActionModel,
|
||||
FormModel,
|
||||
FormItemModel,
|
||||
SubmitActionModel,
|
||||
@ -33,13 +36,25 @@ class PluginDemo extends Plugin {
|
||||
},
|
||||
},
|
||||
subModels: {
|
||||
actions: [
|
||||
{
|
||||
use: 'DeleteActionModel',
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
title: 'Delete',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
{
|
||||
use: 'TableColumnActionsModel',
|
||||
subModels: {
|
||||
actions: [
|
||||
{
|
||||
use: 'ActionModel',
|
||||
use: 'LinkActionModel',
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
@ -49,7 +64,7 @@ class PluginDemo extends Plugin {
|
||||
},
|
||||
},
|
||||
{
|
||||
use: 'ActionModel',
|
||||
use: 'LinkActionModel',
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { CollectionField, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||
import { Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { FormModel } from '../form/form-model';
|
||||
import { ActionModel } from './action-model';
|
||||
|
||||
export class TableColumnModel extends FlowModel {
|
||||
field: Field;
|
||||
field: CollectionField;
|
||||
fieldPath: string;
|
||||
|
||||
getColumnProps() {
|
||||
@ -75,7 +75,12 @@ export class TableColumnActionsModel extends TableColumnModel {
|
||||
return (value, record, index) => (
|
||||
<Space>
|
||||
{this.mapSubModels('actions', (action: ActionModel) => (
|
||||
<FlowModelRenderer key={action.uid} model={action} extraContext={{ record }} />
|
||||
<FlowModelRenderer
|
||||
key={action.uid}
|
||||
model={action.createFork({}, `${record.id || index}`)}
|
||||
showFlowSettings
|
||||
extraContext={{ record }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { Collection, FlowModel, MultiRecordResource } from '@nocobase/flow-engine';
|
||||
import { Button, Dropdown, Table } from 'antd';
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { AddActionModel, Collection, FlowModel, FlowModelRenderer, MultiRecordResource } from '@nocobase/flow-engine';
|
||||
import { Button, Dropdown, Space, Table } from 'antd';
|
||||
import React from 'react';
|
||||
import { ActionModel } from './action-model';
|
||||
import { api } from './api';
|
||||
import { TableColumnModel } from './table-column-model';
|
||||
|
||||
type S = {
|
||||
subModels: {
|
||||
columns: TableColumnModel[];
|
||||
actions: ActionModel[];
|
||||
};
|
||||
};
|
||||
|
||||
@ -36,7 +39,7 @@ export class TableModel extends FlowModel<S> {
|
||||
},
|
||||
items: this.collection.mapFields((field) => {
|
||||
return {
|
||||
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
|
||||
key: `${this.collection.dataSource.key}.${this.collection.name}.${field.name}`,
|
||||
label: field.title,
|
||||
};
|
||||
}),
|
||||
@ -51,10 +54,43 @@ export class TableModel extends FlowModel<S> {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
{this.mapSubModels('actions', (action) => (
|
||||
<FlowModelRenderer
|
||||
model={action}
|
||||
showFlowSettings
|
||||
extraContext={{ currentModel: this, currentResource: this.resource }}
|
||||
/>
|
||||
))}
|
||||
<AddActionModel
|
||||
model={this}
|
||||
subModelKey={'actions'}
|
||||
items={() => [
|
||||
{
|
||||
key: 'action1',
|
||||
label: 'Delete',
|
||||
createModelOptions: {
|
||||
use: 'DeleteActionModel',
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button type="primary" icon={<SettingOutlined />}>
|
||||
Configure actions
|
||||
</Button>
|
||||
</AddActionModel>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={this.resource.getData()}
|
||||
columns={this.getColumns()}
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
onChange: (_, selectedRows) => {
|
||||
this.resource.setSelectedRows(selectedRows);
|
||||
},
|
||||
selectedRowKeys: this.resource.getSelectedRows().map((row) => row.id),
|
||||
}}
|
||||
pagination={{
|
||||
current: this.resource.getMeta('page'),
|
||||
pageSize: this.resource.getMeta('pageSize'),
|
||||
|
@ -20,6 +20,10 @@
|
||||
|
||||
<code src="./demos/register-flow.tsx"></code>
|
||||
|
||||
## 动态默认配置参数
|
||||
|
||||
<code src="./demos/dynamic-default-params.tsx"></code>
|
||||
|
||||
## table block
|
||||
|
||||
<code src="./demos/table-block.tsx"></code>
|
||||
@ -84,3 +88,6 @@
|
||||
|
||||
<code src="./demos/open-required-step-params-dialog.tsx"></code>
|
||||
|
||||
## fork 模型共享示例
|
||||
|
||||
<code src="./demos/fork-flow-model.tsx"></code>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/client",
|
||||
"version": "1.8.0-alpha.5",
|
||||
"version": "1.8.0-alpha.9",
|
||||
"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.9",
|
||||
"@nocobase/sdk": "1.8.0-alpha.9",
|
||||
"@nocobase/utils": "1.8.0-alpha.9",
|
||||
"ahooks": "^3.7.2",
|
||||
"antd": "5.24.2",
|
||||
"antd-style": "3.7.1",
|
||||
@ -65,6 +65,8 @@
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-to-print": "^2.14.7",
|
||||
"sanitize-html": "2.13.0",
|
||||
"tabulator-tables": "^6.3.1",
|
||||
"@types/tabulator-tables": "^6.2.6",
|
||||
"use-deep-compare-effect": "^1.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
@ -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),
|
||||
|
@ -40,7 +40,7 @@ import { DataSourceApplicationProvider } from '../data-source/components/DataSou
|
||||
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
|
||||
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
|
||||
|
||||
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
||||
import { FlowEngine, FlowEngineGlobalsContextProvider, FlowEngineProvider } from '@nocobase/flow-engine';
|
||||
import type { CollectionFieldInterfaceFactory } from '../data-source';
|
||||
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
||||
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||
@ -273,8 +273,14 @@ export class Application {
|
||||
this.use(AntdAppProvider);
|
||||
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
||||
this.use(OpenModeProvider);
|
||||
this.flowEngine.context['app'] = this;
|
||||
this.flowEngine.setContext({
|
||||
app: this,
|
||||
api: this.apiClient,
|
||||
i18n: this.i18n,
|
||||
router: this.router.router,
|
||||
});
|
||||
this.use(FlowEngineProvider, { engine: this.flowEngine });
|
||||
this.use(FlowEngineGlobalsContextProvider);
|
||||
}
|
||||
|
||||
private addReactRouterComponents() {
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { uid } from '@formily/shared';
|
||||
import { Divider, Empty, Input, MenuProps } from 'antd';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '../../../';
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -398,7 +398,7 @@ export const useFilterAction = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const useCreateAction = (actionCallback?: (values: any) => void) => {
|
||||
export const useCreateAction = (actionCallback?: (values: any, collections: any[]) => void) => {
|
||||
const form = useForm();
|
||||
const field = useField();
|
||||
const ctx = useActionContext();
|
||||
@ -410,9 +410,14 @@ export const useCreateAction = (actionCallback?: (values: any) => void) => {
|
||||
await form.submit();
|
||||
field.data = field.data || {};
|
||||
field.data.loading = true;
|
||||
let collections = [];
|
||||
if (!form.values.addAllCollections) {
|
||||
collections = form.values.collections;
|
||||
}
|
||||
delete form.values.collections;
|
||||
const res = await resource.create({ values: form.values });
|
||||
ctx.setVisible(false);
|
||||
actionCallback?.(res?.data?.data);
|
||||
await actionCallback?.(res?.data?.data, collections);
|
||||
await form.reset();
|
||||
field.data.loading = false;
|
||||
refresh();
|
||||
|
@ -9,7 +9,8 @@
|
||||
|
||||
import { Plugin } from '../application/Plugin';
|
||||
|
||||
import { InheritanceCollectionMixin } from './mixins/InheritanceCollectionMixin';
|
||||
import { DataSource } from '../data-source/data-source/DataSource';
|
||||
import { DEFAULT_DATA_SOURCE_KEY, DEFAULT_DATA_SOURCE_TITLE } from '../data-source/data-source/DataSourceManager';
|
||||
import {
|
||||
CheckboxFieldInterface,
|
||||
CheckboxGroupFieldInterface,
|
||||
@ -53,14 +54,13 @@ import {
|
||||
UrlFieldInterface,
|
||||
UUIDFieldInterface,
|
||||
} from './interfaces';
|
||||
import { InheritanceCollectionMixin } from './mixins/InheritanceCollectionMixin';
|
||||
import {
|
||||
GeneralCollectionTemplate,
|
||||
SqlCollectionTemplate,
|
||||
TreeCollectionTemplate,
|
||||
ViewCollectionTemplate,
|
||||
} from './templates';
|
||||
import { DEFAULT_DATA_SOURCE_KEY, DEFAULT_DATA_SOURCE_TITLE } from '../data-source/data-source/DataSourceManager';
|
||||
import { DataSource } from '../data-source/data-source/DataSource';
|
||||
|
||||
class MainDataSource extends DataSource {
|
||||
async getDataSource() {
|
||||
@ -72,6 +72,7 @@ class MainDataSource extends DataSource {
|
||||
const collections = service?.data?.data || [];
|
||||
|
||||
return {
|
||||
key: 'main',
|
||||
collections,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 = [
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -83,8 +83,18 @@ export abstract class DataSource {
|
||||
|
||||
abstract getDataSource(): Promise<Omit<Partial<DataSourceOptions>, 'key'>> | Omit<Partial<DataSourceOptions>, 'key'>;
|
||||
|
||||
get flowEngineDataSourceManager() {
|
||||
return this.app.flowEngine?.context?.dataSourceManager;
|
||||
}
|
||||
|
||||
async reload() {
|
||||
const dataSource = await this.getDataSource();
|
||||
const flowEngineDataSourceManager = this.flowEngineDataSourceManager;
|
||||
if (flowEngineDataSourceManager) {
|
||||
flowEngineDataSourceManager.upsertDataSource(this.options);
|
||||
const ds = flowEngineDataSourceManager.getDataSource(this.key);
|
||||
ds.upsertCollections(dataSource.collections || []);
|
||||
}
|
||||
this.setOptions(dataSource);
|
||||
this.collectionManager.setCollections(dataSource.collections || []);
|
||||
this.reloadCallbacks.forEach((callback) => callback(dataSource.collections));
|
||||
|
@ -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;
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
import { FlowModel, IFlowModelRepository } from '@nocobase/flow-engine';
|
||||
import _ from 'lodash';
|
||||
import { Application } from '../application';
|
||||
|
||||
export class MockFlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||
get models() {
|
||||
@ -26,6 +27,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 +64,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;
|
||||
}
|
||||
@ -77,3 +100,32 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||
constructor(private app: Application) {}
|
||||
async findOne(query) {
|
||||
const response = await this.app.apiClient.request({
|
||||
url: 'flowModels:findOne',
|
||||
params: _.pick(query, ['uid', 'parentId']),
|
||||
});
|
||||
return response.data?.data;
|
||||
}
|
||||
|
||||
async save(model: FlowModel) {
|
||||
const response = await this.app.apiClient.request({
|
||||
method: 'POST',
|
||||
url: 'flowModels:save',
|
||||
data: model.serialize(),
|
||||
});
|
||||
return response.data?.data;
|
||||
}
|
||||
|
||||
async destroy(uid: string) {
|
||||
await this.app.apiClient.request({
|
||||
method: 'POST',
|
||||
url: 'flowModels:destroy',
|
||||
params: { filterByTk: uid },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -7,49 +7,65 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FlowModelRenderer, useFlowEngine, useFlowModel } from '@nocobase/flow-engine';
|
||||
import { FlowModelRenderer, useFlowEngine, useFlowModelById } from '@nocobase/flow-engine';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function InternalFlowPage({ uid }) {
|
||||
const model = useFlowModel(uid);
|
||||
return <FlowModelRenderer model={model} showFlowSettings hideRemoveInSettings />;
|
||||
function InternalFlowPage({ uid, sharedContext }) {
|
||||
const model = useFlowModelById(uid);
|
||||
return (
|
||||
<FlowModelRenderer
|
||||
model={model}
|
||||
sharedContext={sharedContext}
|
||||
showFlowSettings={{ showBackground: false, showBorder: false }}
|
||||
hideRemoveInSettings
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const FlowPage = () => {
|
||||
export const FlowRoute = () => {
|
||||
const params = useParams();
|
||||
return <FlowPageComponent uid={params.name} />;
|
||||
return <FlowPage uid={`r_${params.name}`} />;
|
||||
};
|
||||
|
||||
export const FlowPageComponent = ({ uid }) => {
|
||||
export const FlowPage = (props) => {
|
||||
const { uid, parentId, sharedContext } = props;
|
||||
const flowEngine = useFlowEngine();
|
||||
const { loading } = useRequest(
|
||||
() => {
|
||||
return flowEngine.loadOrCreateModel({
|
||||
uid: uid,
|
||||
use: 'PageFlowModel',
|
||||
const { loading, data } = useRequest(
|
||||
async () => {
|
||||
const options = {
|
||||
uid,
|
||||
use: 'PageModel',
|
||||
subModels: {
|
||||
tabs: [
|
||||
{
|
||||
use: 'PageTabFlowModel',
|
||||
use: 'PageTabModel',
|
||||
subModels: {
|
||||
grid: {
|
||||
use: 'BlockGridFlowModel',
|
||||
use: 'BlockGridModel',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
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} />;
|
||||
return <InternalFlowPage uid={data.uid} sharedContext={sharedContext} />;
|
||||
};
|
||||
|
96
packages/core/client/src/flow/actions/afterSuccessAction.tsx
Normal file
96
packages/core/client/src/flow/actions/afterSuccessAction.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import { useGlobalVariable } from '../../application/hooks/useGlobalVariable';
|
||||
import { BlocksSelector } from '../../schema-component/antd/action/Action.Designer';
|
||||
import { useAfterSuccessOptions } from '../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions';
|
||||
|
||||
const fieldNames = {
|
||||
value: 'value',
|
||||
label: 'label',
|
||||
};
|
||||
const useVariableProps = () => {
|
||||
const environmentVariables = useGlobalVariable('$env');
|
||||
const scope = useAfterSuccessOptions();
|
||||
return {
|
||||
scope: [environmentVariables, ...scope].filter(Boolean),
|
||||
fieldNames,
|
||||
};
|
||||
};
|
||||
|
||||
export const afterSuccessAction = {
|
||||
title: '提交成功后',
|
||||
uiSchema: {
|
||||
successMessage: {
|
||||
title: 'Popup message',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
'x-component-props': {},
|
||||
},
|
||||
manualClose: {
|
||||
title: 'Message popup close method',
|
||||
enum: [
|
||||
{ label: 'Automatic close', value: false },
|
||||
{ label: 'Manually close', value: true },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {},
|
||||
},
|
||||
redirecting: {
|
||||
title: 'Then',
|
||||
'x-hidden': true,
|
||||
enum: [
|
||||
{ label: 'Stay on current page', value: false },
|
||||
{ label: 'Redirect to', value: true },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {},
|
||||
'x-reactions': {
|
||||
target: 'redirectTo',
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
actionAfterSuccess: {
|
||||
title: 'Action after successful submission',
|
||||
enum: [
|
||||
{ label: 'Stay on the current popup or page', value: 'stay' },
|
||||
{ label: 'Return to the previous popup or page', value: 'previous' },
|
||||
{ label: 'Redirect to', value: 'redirect' },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
'x-component-props': {},
|
||||
'x-reactions': {
|
||||
target: 'redirectTo',
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: "{{$self.value==='redirect'}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
redirectTo: {
|
||||
title: 'Link',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.TextArea',
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
'x-use-component-props': () => useVariableProps(),
|
||||
},
|
||||
blocksToRefresh: {
|
||||
type: 'array',
|
||||
title: 'Refresh data blocks',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-use-decorator-props': () => {
|
||||
return {
|
||||
tooltip: 'After successful submission, the selected data blocks will be automatically refreshed.',
|
||||
};
|
||||
},
|
||||
'x-component': BlocksSelector,
|
||||
// 'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {},
|
||||
};
|
54
packages/core/client/src/flow/actions/confirm.tsx
Normal file
54
packages/core/client/src/flow/actions/confirm.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { defineAction } from '@nocobase/flow-engine';
|
||||
|
||||
export const confirm = defineAction({
|
||||
name: 'confirm',
|
||||
title: '二次确认',
|
||||
uiSchema: {
|
||||
enable: {
|
||||
type: 'boolean',
|
||||
title: 'Enable secondary confirmation',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: 'Delete record',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
title: 'Content',
|
||||
default: 'Are you sure you want to delete it?',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
enable: true,
|
||||
title: 'Delete record',
|
||||
content: 'Are you sure you want to delete it?',
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
if (params.enable) {
|
||||
const confirmed = await ctx.globals.modal.confirm({
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.exit();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
12
packages/core/client/src/flow/actions/index.ts
Normal file
12
packages/core/client/src/flow/actions/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './confirm';
|
||||
export * from './popup';
|
||||
//
|
94
packages/core/client/src/flow/actions/openLinkAction.tsx
Normal file
94
packages/core/client/src/flow/actions/openLinkAction.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { Variable } from '../../schema-component/antd/variable/Variable';
|
||||
|
||||
export const openLinkAction = {
|
||||
title: '编辑链接',
|
||||
uiSchema: {
|
||||
url: {
|
||||
title: 'URL',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': Variable.TextArea,
|
||||
description: 'Do not concatenate search params in the URL',
|
||||
},
|
||||
params: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayItems',
|
||||
'x-decorator': 'FormItem',
|
||||
title: `Search parameters`,
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
space: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
flexWrap: 'nowrap',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
className: css`
|
||||
& > .ant-space-item:first-child,
|
||||
& > .ant-space-item:last-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`,
|
||||
},
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: `{{t("Name")}}`,
|
||||
},
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': Variable.TextArea,
|
||||
'x-component-props': {
|
||||
placeholder: `{{t("Value")}}`,
|
||||
useTypedConstant: true,
|
||||
changeOnSelect: true,
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'ArrayItems.Remove',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
add: {
|
||||
type: 'void',
|
||||
title: 'Add parameter',
|
||||
'x-component': 'ArrayItems.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
openInNewWindow: {
|
||||
type: 'boolean',
|
||||
'x-content': 'Open in new window',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.globals.modal.confirm({
|
||||
title: `TODO`,
|
||||
content: JSON.stringify(params, null, 2),
|
||||
});
|
||||
},
|
||||
};
|
65
packages/core/client/src/flow/actions/openModeAction.tsx
Normal file
65
packages/core/client/src/flow/actions/openModeAction.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { FlowPage } from '../FlowPage';
|
||||
|
||||
export const openModeAction = {
|
||||
title: '打开方式',
|
||||
uiSchema: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
title: '打开方式',
|
||||
enum: [
|
||||
{ label: 'Drawer', value: 'drawer' },
|
||||
{ label: 'Modal', value: 'modal' },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
},
|
||||
size: {
|
||||
type: 'string',
|
||||
title: '弹窗尺寸',
|
||||
enum: [
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '大', value: 'large' },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
},
|
||||
},
|
||||
defaultParams(ctx) {
|
||||
return {
|
||||
mode: 'drawer',
|
||||
size: 'medium',
|
||||
};
|
||||
},
|
||||
handler(ctx, params) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let currentDrawer: any;
|
||||
|
||||
function DrawerContent() {
|
||||
return (
|
||||
<div>
|
||||
<FlowPage
|
||||
parentId={ctx.model.uid}
|
||||
sharedContext={{
|
||||
...ctx.extra,
|
||||
currentDrawer,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sizeToWidthMap: Record<string, number> = {
|
||||
small: 480,
|
||||
medium: 800,
|
||||
large: 1200,
|
||||
};
|
||||
|
||||
currentDrawer = ctx.globals[params.mode].open({
|
||||
title: '命令式 Drawer',
|
||||
width: sizeToWidthMap[params.size],
|
||||
content: <DrawerContent />,
|
||||
});
|
||||
},
|
||||
};
|
74
packages/core/client/src/flow/actions/popup.tsx
Normal file
74
packages/core/client/src/flow/actions/popup.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 { defineAction } from '@nocobase/flow-engine';
|
||||
import React from 'react';
|
||||
import { FlowPage } from '../FlowPage';
|
||||
|
||||
export const popup = defineAction({
|
||||
name: 'popup',
|
||||
title: '弹窗配置',
|
||||
uiSchema: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
title: '打开方式',
|
||||
enum: [
|
||||
{ label: 'Drawer', value: 'drawer' },
|
||||
{ label: 'Modal', value: 'modal' },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
},
|
||||
size: {
|
||||
type: 'string',
|
||||
title: '弹窗尺寸',
|
||||
enum: [
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '大', value: 'large' },
|
||||
],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
mode: 'drawer',
|
||||
size: 'medium',
|
||||
},
|
||||
handler(ctx, params) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let currentDrawer: any;
|
||||
|
||||
function DrawerContent() {
|
||||
return (
|
||||
<div>
|
||||
<FlowPage
|
||||
parentId={ctx.model.uid}
|
||||
sharedContext={{
|
||||
...params.sharedContext,
|
||||
currentDrawer,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const sizeToWidthMap: Record<string, number> = {
|
||||
small: 480,
|
||||
medium: 800,
|
||||
large: 1200,
|
||||
};
|
||||
|
||||
currentDrawer = ctx.globals[params.mode || 'drawer'].open({
|
||||
title: '命令式 Drawer',
|
||||
width: sizeToWidthMap[params.size || 'medium'],
|
||||
content: <DrawerContent />,
|
||||
});
|
||||
},
|
||||
});
|
@ -0,0 +1,22 @@
|
||||
export const refreshOnCompleteAction = {
|
||||
title: '执行后刷新数据',
|
||||
uiSchema: {
|
||||
enable: {
|
||||
type: 'boolean',
|
||||
title: 'Enable refresh',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
},
|
||||
defaultParams(ctx) {
|
||||
return {
|
||||
enable: true,
|
||||
};
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
if (params.enable) {
|
||||
await ctx.extra.currentResource.refresh();
|
||||
ctx.globals.message.success('Data refreshed successfully.');
|
||||
}
|
||||
},
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
export const secondaryConfirmationAction = {
|
||||
title: '二次确认',
|
||||
uiSchema: {
|
||||
enable: {
|
||||
type: 'boolean',
|
||||
title: 'Enable secondary confirmation',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: 'Delete record',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
title: 'Content',
|
||||
default: 'Are you sure you want to delete it?',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.TextArea',
|
||||
},
|
||||
},
|
||||
defaultParams(ctx) {
|
||||
return {
|
||||
enable: true,
|
||||
title: 'Delete record',
|
||||
content: 'Are you sure you want to delete it?',
|
||||
};
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
if (params.enable) {
|
||||
const confirmed = await ctx.globals.modal.confirm({
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
ctx.exit();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* 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 { Popover } from 'antd';
|
||||
import React, { CSSProperties, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
|
||||
const getContentWidth = (el: HTMLElement) => {
|
||||
if (el) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(el);
|
||||
const contentWidth = range.getBoundingClientRect().width;
|
||||
return contentWidth;
|
||||
}
|
||||
};
|
||||
const ellipsisDefaultStyle: CSSProperties = {
|
||||
overflow: 'hidden',
|
||||
overflowWrap: 'break-word',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
wordBreak: 'break-all',
|
||||
};
|
||||
|
||||
const isOverflowTooltip = (el: HTMLElement) => {
|
||||
if (!el) return false;
|
||||
const contentWidth = getContentWidth(el);
|
||||
const offsetWidth = el.offsetWidth;
|
||||
return contentWidth > offsetWidth;
|
||||
};
|
||||
|
||||
interface IEllipsisWithTooltipProps {
|
||||
ellipsis: boolean;
|
||||
popoverContent: unknown;
|
||||
children: any;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
const popoverStyle = {
|
||||
width: 300,
|
||||
overflow: 'auto',
|
||||
maxHeight: 400,
|
||||
};
|
||||
export const EllipsisWithTooltip = forwardRef((props: Partial<IEllipsisWithTooltipProps>, ref: any) => {
|
||||
const [ellipsis, setEllipsis] = useState(false);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const elRef: any = useRef();
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
setPopoverVisible: setVisible,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>): void => {
|
||||
const el = e.target as any;
|
||||
const isShowTooltips = isOverflowTooltip(elRef.current);
|
||||
if (isShowTooltips) {
|
||||
setEllipsis(el.scrollWidth >= el.clientWidth);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const divContent = useMemo(
|
||||
() =>
|
||||
props.ellipsis ? (
|
||||
<div ref={elRef} role={props.role} style={ellipsisDefaultStyle} onMouseEnter={handleMouseEnter}>
|
||||
{props.children}
|
||||
</div>
|
||||
) : (
|
||||
props.children
|
||||
),
|
||||
[handleMouseEnter, props.children, props.ellipsis, props.role],
|
||||
);
|
||||
|
||||
if (!props.ellipsis || !ellipsis) {
|
||||
return divContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={ellipsis && visible}
|
||||
onOpenChange={(visible) => {
|
||||
setVisible(ellipsis && visible);
|
||||
}}
|
||||
content={<div style={popoverStyle}>{props.popoverContent || props.children}</div>}
|
||||
>
|
||||
{divContent}
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
EllipsisWithTooltip.displayName = 'EllipsisWithTooltip';
|
140
packages/core/client/src/flow/components/ExpiresRadio/index.tsx
Normal file
140
packages/core/client/src/flow/components/ExpiresRadio/index.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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 { css } from '@emotion/css';
|
||||
import dayjs from 'dayjs';
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
import { useBoolean } from 'ahooks';
|
||||
import { Input, Radio, Space, theme } from 'antd';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
|
||||
const date = dayjs();
|
||||
|
||||
const spaceCSS = css`
|
||||
width: 100%;
|
||||
& > .ant-space-item {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
const DateFormatCom = (props?) => {
|
||||
const date = dayjs();
|
||||
return (
|
||||
<div style={{ display: 'inline-flex' }}>
|
||||
<span>{props.format}</span>
|
||||
<DateTimeFormatPreview content={date.format(props.format)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DateTimeFormatPreview = ({ content }) => {
|
||||
const { token } = theme.useToken();
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: token.colorBgTextHover,
|
||||
marginLeft: token.marginMD,
|
||||
lineHeight: '1',
|
||||
padding: token.paddingXXS,
|
||||
borderRadius: token.borderRadiusOuter,
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const InternalExpiresRadio = (props) => {
|
||||
console.log(props);
|
||||
const { onChange, defaultValue, formats, picker } = props;
|
||||
const [isCustom, { setFalse, setTrue }] = useBoolean(props.value && !formats.includes(props.value));
|
||||
const [targetValue, setTargetValue] = useState(
|
||||
props.value && !formats.includes(props.value) ? props.value : defaultValue,
|
||||
);
|
||||
const [customFormatPreview, setCustomFormatPreview] = useState(targetValue ? date.format(targetValue) : null);
|
||||
const onSelectChange = (v) => {
|
||||
if (v.target.value === 'custom') {
|
||||
setTrue();
|
||||
onChange(targetValue);
|
||||
} else {
|
||||
setFalse();
|
||||
onChange(v.target.value);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!formats.includes(props.value)) {
|
||||
setTrue();
|
||||
} else {
|
||||
setFalse();
|
||||
}
|
||||
setTargetValue(props.value && !formats.includes(props.value) ? props.value : defaultValue);
|
||||
}, [props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomFormatPreview(targetValue ? date.format(targetValue) : null);
|
||||
}, [targetValue]);
|
||||
|
||||
return (
|
||||
<Space className={spaceCSS}>
|
||||
<Radio.Group value={isCustom ? 'custom' : props.value} onChange={onSelectChange}>
|
||||
<Space direction="vertical">
|
||||
{props.options.map((v) => {
|
||||
if (v.value === 'custom') {
|
||||
return (
|
||||
<Radio value={v.value} key={v.value}>
|
||||
<Input
|
||||
style={{ width: '150px' }}
|
||||
value={targetValue}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) {
|
||||
setCustomFormatPreview(date.format(e.target.value));
|
||||
} else {
|
||||
setCustomFormatPreview(null);
|
||||
}
|
||||
if (isCustom) {
|
||||
onChange(e.target.value);
|
||||
}
|
||||
setTargetValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<DateTimeFormatPreview content={customFormatPreview} />
|
||||
</Radio>
|
||||
);
|
||||
}
|
||||
if (!picker || picker === 'date') {
|
||||
return (
|
||||
<Radio value={v.value} key={v.value} aria-label={v.value}>
|
||||
<span role="button" aria-label={v.value}>
|
||||
{v.label}
|
||||
</span>
|
||||
</Radio>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
const ExpiresRadio = connect(
|
||||
InternalExpiresRadio,
|
||||
mapProps(
|
||||
{
|
||||
dataSource: 'options',
|
||||
},
|
||||
(props) => {
|
||||
return {
|
||||
...props,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export { ExpiresRadio, DateFormatCom };
|
@ -0,0 +1,197 @@
|
||||
/**
|
||||
* 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 { isValid } from '@formily/shared';
|
||||
import { toFixedByStep } from '@nocobase/utils/client';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { format } from 'd3-format';
|
||||
import * as math from 'mathjs';
|
||||
import React, { useMemo } from 'react';
|
||||
import { toString } from 'lodash';
|
||||
import { connect, mapProps } from '@formily/react';
|
||||
|
||||
function countDecimalPlaces(value) {
|
||||
const strValue = toString(value);
|
||||
|
||||
// 检查是否包含小数点
|
||||
if (!strValue.includes('.')) return 0;
|
||||
|
||||
// 获取小数部分并去除末尾的零
|
||||
const decimalPart = strValue.split('.')[1].replace(/0+$/, '');
|
||||
|
||||
return decimalPart.length;
|
||||
}
|
||||
const separators = {
|
||||
'0,0.00': { thousands: ',', decimal: '.' },
|
||||
'0.0,00': { thousands: '.', decimal: ',' },
|
||||
'0 0,00': { thousands: ' ', decimal: '.' },
|
||||
'0.00': { thousands: '', decimal: '.' }, // 没有千位分隔符
|
||||
};
|
||||
//分隔符换算
|
||||
function formatNumberWithSeparator(value, format = '0.00', step = 1, formatStyle?) {
|
||||
if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
|
||||
return formatBigNumberWithSeparator(value, format, step, formatStyle);
|
||||
}
|
||||
let number = value;
|
||||
|
||||
if (formatStyle) {
|
||||
number = Number(value);
|
||||
}
|
||||
let formattedNumber = '';
|
||||
|
||||
if (separators[format]) {
|
||||
const { thousands, decimal } = separators[format];
|
||||
formattedNumber = number
|
||||
.toLocaleString('en-US', {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: step,
|
||||
maximumFractionDigits: step,
|
||||
})
|
||||
.replace(/,/g, 'comma_placeholder')
|
||||
.replace(/\./g, 'dot_placeholder')
|
||||
.replace(/comma_placeholder/g, thousands)
|
||||
.replace(/dot_placeholder/g, decimal);
|
||||
} else {
|
||||
formattedNumber = number.toString();
|
||||
}
|
||||
return formattedNumber;
|
||||
}
|
||||
//大字段分隔符换算
|
||||
function formatBigNumberWithSeparator(value, format = '0.00', step = 1, formatStyle?) {
|
||||
let number = value;
|
||||
|
||||
if (formatStyle) {
|
||||
number = new BigNumber(value).toString();
|
||||
}
|
||||
|
||||
let formattedNumber = '';
|
||||
if (separators[format]) {
|
||||
const { thousands, decimal } = separators[format];
|
||||
const [integerPart, initFractionalPart] = number.toString().split('.');
|
||||
let fractionalPart = initFractionalPart;
|
||||
// 格式化整数部分
|
||||
const formattedIntegerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousands);
|
||||
// 处理小数部分
|
||||
if (fractionalPart && step) {
|
||||
fractionalPart = fractionalPart.substring(0, step);
|
||||
formattedNumber = `${formattedIntegerPart}${decimal}${fractionalPart}`;
|
||||
} else {
|
||||
formattedNumber = formattedIntegerPart;
|
||||
}
|
||||
} else {
|
||||
formattedNumber = number.toString();
|
||||
}
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
//单位换算
|
||||
function formatUnitConversion(value, operator = '*', multiplier?: number) {
|
||||
if (!multiplier) {
|
||||
return value;
|
||||
}
|
||||
let result;
|
||||
|
||||
if (operator === '*') {
|
||||
result = value * multiplier;
|
||||
} else if (operator === '/') {
|
||||
if (multiplier !== 0) {
|
||||
result = value / multiplier;
|
||||
} else {
|
||||
console.error('Error: Division by zero.');
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
console.error("Error: Invalid operator. Use '*' for multiplication or '/' for division.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return math.round(result, 9);
|
||||
}
|
||||
|
||||
//科学计数法显示
|
||||
function scientificNotation(number, decimalPlaces, separator = '.') {
|
||||
const formatter = format(`.${decimalPlaces}e`);
|
||||
const formattedNumber = formatter(number).replace('.', separator);
|
||||
|
||||
// 匹配科学计数法中的指数部分,判断正负情况
|
||||
const result = formattedNumber.replace(/e([+-]?\d+)/, (match, exponent) => {
|
||||
if (exponent.startsWith('+')) {
|
||||
// 正数指数,不显示符号
|
||||
return `×10<sup>${exponent.slice(1)}</sup>`;
|
||||
} else {
|
||||
// 负数指数,显示 "-" 符号
|
||||
return `×10<sup>-${exponent.slice(1)}</sup>`;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatNumber(props) {
|
||||
const { step, formatStyle = 'normal', value, unitConversion, unitConversionType, separator = '0,0.00' } = props;
|
||||
|
||||
if (!isValid(value)) {
|
||||
return null;
|
||||
}
|
||||
//单位换算
|
||||
const unitData = formatUnitConversion(value, unitConversionType, unitConversion);
|
||||
//精度换算
|
||||
const precisionData = toFixedByStep(unitData, step);
|
||||
let result;
|
||||
//分隔符换算
|
||||
result = formatNumberWithSeparator(precisionData, separator, countDecimalPlaces(step), formatStyle);
|
||||
if (formatStyle === 'scientifix') {
|
||||
//科学计数显示
|
||||
result = scientificNotation(Number(unitData), countDecimalPlaces(step), separators?.[separator]?.['decimal']);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface InputNumberReadPrettyProps {
|
||||
formatStyle?: 'normal' | 'scientifix';
|
||||
unitConversion?: number;
|
||||
/**
|
||||
* @default '*'
|
||||
*/
|
||||
unitConversionType?: '*' | '/';
|
||||
/**
|
||||
* @default '0.00'
|
||||
*/
|
||||
separator?: '0,0.00' | '0.0,00' | '0 0,00' | '0.00';
|
||||
step?: number;
|
||||
value?: any;
|
||||
addonBefore?: React.ReactNode;
|
||||
addonAfter?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const InputNumberReadPretty = connect(
|
||||
(props: InputNumberReadPrettyProps) => {
|
||||
const { step, formatStyle, value, addonBefore, addonAfter, unitConversion, unitConversionType, separator } = props;
|
||||
const result = useMemo(() => {
|
||||
return formatNumber({ step, formatStyle, value, unitConversion, unitConversionType, separator });
|
||||
}, [step, formatStyle, value, unitConversion, unitConversionType, separator]);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{addonBefore}
|
||||
<span dangerouslySetInnerHTML={{ __html: result }} />
|
||||
{addonAfter}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
mapProps((props) => {
|
||||
return {
|
||||
...props,
|
||||
};
|
||||
}),
|
||||
);
|
11
packages/core/client/src/flow/components/index.tsx
Normal file
11
packages/core/client/src/flow/components/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export * from './EllipsisWithTooltip';
|
||||
export * from './ExpiresRadio';
|
169
packages/core/client/src/flow/flowSetting/DateTimeFormat.tsx
Normal file
169
packages/core/client/src/flow/flowSetting/DateTimeFormat.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 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 { css } from '@emotion/css';
|
||||
import { getPickerFormat } from '@nocobase/utils/client';
|
||||
import { ExpiresRadio, DateFormatCom } from '../components';
|
||||
|
||||
export const DateTimeFormat = {
|
||||
title: 'Date display format',
|
||||
name: 'dateDisplayFormat',
|
||||
uiSchema: {
|
||||
picker: {
|
||||
type: 'string',
|
||||
title: '{{t("Picker")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
description: '{{ t("Switching the picker, the value and default value will be cleared") }}',
|
||||
enum: [
|
||||
{
|
||||
label: '{{t("Date")}}',
|
||||
value: 'date',
|
||||
},
|
||||
|
||||
{
|
||||
label: '{{t("Month")}}',
|
||||
value: 'month',
|
||||
},
|
||||
{
|
||||
label: '{{t("Quarter")}}',
|
||||
value: 'quarter',
|
||||
},
|
||||
{
|
||||
label: '{{t("Year")}}',
|
||||
value: 'year',
|
||||
},
|
||||
],
|
||||
},
|
||||
dateFormat: {
|
||||
type: 'string',
|
||||
title: '{{t("Date format")}}',
|
||||
'x-component': ExpiresRadio,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
.ant-radio-wrapper {
|
||||
display: flex;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
`,
|
||||
defaultValue: 'dddd',
|
||||
formats: ['MMMM Do YYYY', 'YYYY-MM-DD', 'MM/DD/YY', 'YYYY/MM/DD', 'DD/MM/YYYY'],
|
||||
},
|
||||
enum: [
|
||||
{
|
||||
label: DateFormatCom({ format: 'MMMM Do YYYY' }),
|
||||
value: 'MMMM Do YYYY',
|
||||
},
|
||||
{
|
||||
label: DateFormatCom({ format: 'YYYY-MM-DD' }),
|
||||
value: 'YYYY-MM-DD',
|
||||
},
|
||||
{
|
||||
label: DateFormatCom({ format: 'MM/DD/YY' }),
|
||||
value: 'MM/DD/YY',
|
||||
},
|
||||
{
|
||||
label: DateFormatCom({ format: 'YYYY/MM/DD' }),
|
||||
value: 'YYYY/MM/DD',
|
||||
},
|
||||
{
|
||||
label: DateFormatCom({ format: 'DD/MM/YYYY' }),
|
||||
value: 'DD/MM/YYYY',
|
||||
},
|
||||
{
|
||||
label: 'custom',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
'x-reactions': [
|
||||
(field) => {
|
||||
const { picker } = field.form.values;
|
||||
field.value = getPickerFormat(picker);
|
||||
field.setComponentProps({ picker });
|
||||
},
|
||||
],
|
||||
},
|
||||
showTime: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-content': '{{t("Show time")}}',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['picker'],
|
||||
fulfill: {
|
||||
state: {
|
||||
hidden: `{{ $form.values.picker !== 'date' || collectionField.type!== 'date' }}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
timeFormat: {
|
||||
type: 'string',
|
||||
title: '{{t("Time format")}}',
|
||||
'x-component': ExpiresRadio,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
className: css`
|
||||
margin-bottom: 0px;
|
||||
`,
|
||||
},
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
color: red;
|
||||
.ant-radio-wrapper {
|
||||
display: flex;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
`,
|
||||
defaultValue: 'h:mm a',
|
||||
formats: ['hh:mm:ss a', 'HH:mm:ss'],
|
||||
timeFormat: true,
|
||||
},
|
||||
'x-reactions': [
|
||||
(field) => {
|
||||
const { showTime } = field.form.values || {};
|
||||
field.hidden = !showTime;
|
||||
},
|
||||
],
|
||||
enum: [
|
||||
{
|
||||
label: DateFormatCom({ format: 'hh:mm:ss a' }),
|
||||
value: 'hh:mm:ss a',
|
||||
},
|
||||
{
|
||||
label: DateFormatCom({ format: 'HH:mm:ss' }),
|
||||
value: 'HH:mm:ss',
|
||||
},
|
||||
{
|
||||
label: 'custom',
|
||||
value: 'custom',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.flowEngine.flowSettings.registerScopes({
|
||||
collectionField: ctx.model.collectionField,
|
||||
});
|
||||
ctx.model.setProps({ ...params });
|
||||
},
|
||||
defaultParams: (ctx) => {
|
||||
const { showTime, dateFormat, timeFormat, picker } = ctx.model.field.componentProps || {};
|
||||
return {
|
||||
picker: picker || 'date',
|
||||
dateFormat: dateFormat || 'YYYY-MM-DD',
|
||||
timeFormat: timeFormat,
|
||||
showTime,
|
||||
};
|
||||
},
|
||||
};
|
106
packages/core/client/src/flow/formily/ReactiveField.tsx
Normal file
106
packages/core/client/src/flow/formily/ReactiveField.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Form, GeneralField, isVoidField } from '@formily/core';
|
||||
import { RenderPropsChildren, SchemaComponentsContext } from '@formily/react';
|
||||
import { toJS } from '@formily/reactive';
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import { FormPath, isFn } from '@formily/shared';
|
||||
import React, { Fragment, useContext } from 'react';
|
||||
interface IReactiveFieldProps {
|
||||
field: GeneralField;
|
||||
children?: RenderPropsChildren<GeneralField>;
|
||||
}
|
||||
|
||||
const mergeChildren = (children: RenderPropsChildren<GeneralField>, content: React.ReactNode) => {
|
||||
if (!children && !content) return;
|
||||
if (isFn(children)) return;
|
||||
return (
|
||||
<Fragment>
|
||||
{children}
|
||||
{content}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const isValidComponent = (target: any) => target && (typeof target === 'object' || typeof target === 'function');
|
||||
|
||||
const renderChildren = (children: RenderPropsChildren<GeneralField>, field?: GeneralField, form?: Form) =>
|
||||
isFn(children) ? children(field, form) : children;
|
||||
|
||||
const ReactiveInternal: React.FC<IReactiveFieldProps> = (props) => {
|
||||
const components = useContext(SchemaComponentsContext);
|
||||
if (!props.field) {
|
||||
return <Fragment>{renderChildren(props.children)}</Fragment>;
|
||||
}
|
||||
const field = props.field;
|
||||
const content = mergeChildren(
|
||||
renderChildren(props.children, field, field.form),
|
||||
field.content ?? field.componentProps.children,
|
||||
);
|
||||
if (field.display !== 'visible') return null;
|
||||
|
||||
const getComponent = (target: any) => {
|
||||
return isValidComponent(target) ? target : FormPath.getIn(components, target) ?? target;
|
||||
};
|
||||
|
||||
const renderDecorator = (children: React.ReactNode) => {
|
||||
if (!field.decoratorType) {
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
return React.createElement(getComponent(field.decoratorType), toJS(field.decoratorProps), children);
|
||||
};
|
||||
|
||||
const renderComponent = () => {
|
||||
if (!field.componentType) return content;
|
||||
const value = !isVoidField(field) ? field.value : undefined;
|
||||
const onChange = !isVoidField(field)
|
||||
? (...args: any[]) => {
|
||||
field.onInput(...args);
|
||||
field.componentProps?.onChange?.(...args);
|
||||
}
|
||||
: field.componentProps?.onChange;
|
||||
const onFocus = !isVoidField(field)
|
||||
? (...args: any[]) => {
|
||||
field.onFocus(...args);
|
||||
field.componentProps?.onFocus?.(...args);
|
||||
}
|
||||
: field.componentProps?.onFocus;
|
||||
const onBlur = !isVoidField(field)
|
||||
? (...args: any[]) => {
|
||||
field.onBlur(...args);
|
||||
field.componentProps?.onBlur?.(...args);
|
||||
}
|
||||
: field.componentProps?.onBlur;
|
||||
const disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined;
|
||||
const readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined;
|
||||
return React.createElement(
|
||||
getComponent(field.componentType),
|
||||
{
|
||||
disabled,
|
||||
readOnly,
|
||||
...toJS(field.componentProps),
|
||||
value,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
},
|
||||
content,
|
||||
);
|
||||
};
|
||||
|
||||
return renderDecorator(renderComponent());
|
||||
};
|
||||
|
||||
ReactiveInternal.displayName = 'ReactiveField';
|
||||
|
||||
export const ReactiveField = observer(ReactiveInternal, {
|
||||
forwardRef: true,
|
||||
});
|
8
packages/core/client/src/flow/formily/index.tsx
Normal file
8
packages/core/client/src/flow/formily/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
@ -10,44 +10,37 @@
|
||||
import { DataSource, DataSourceManager, FlowModel } from '@nocobase/flow-engine';
|
||||
import _ from 'lodash';
|
||||
import { Plugin } from '../application/Plugin';
|
||||
import * as actions from './actions';
|
||||
import { FlowEngineRunner } from './FlowEngineRunner';
|
||||
import { MockFlowModelRepository } from './FlowModelRepository';
|
||||
import { FlowPage } from './FlowPage';
|
||||
import { FlowModelRepository, MockFlowModelRepository } from './FlowModelRepository';
|
||||
import { FlowRoute } from './FlowPage';
|
||||
import { DateTimeFormat } from './flowSetting/DateTimeFormat';
|
||||
import * as models from './models';
|
||||
|
||||
export class PluginFlowEngine extends Plugin {
|
||||
async load() {
|
||||
this.app.addComponents({ FlowPage });
|
||||
this.app.flowEngine.setModelRepository(new MockFlowModelRepository());
|
||||
this.app.addComponents({ FlowRoute });
|
||||
// this.app.flowEngine.setModelRepository(new MockFlowModelRepository());
|
||||
this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
|
||||
const filteredModels = Object.fromEntries(
|
||||
Object.entries(models).filter(
|
||||
([, ModelClass]) => typeof ModelClass === 'function' && ModelClass.prototype instanceof FlowModel,
|
||||
),
|
||||
);
|
||||
console.log('Registering flow models:', Object.keys(filteredModels));
|
||||
) as Record<string, typeof FlowModel>;
|
||||
// console.log('Registering flow models:', Object.keys(filteredModels));
|
||||
this.flowEngine.registerModels(filteredModels);
|
||||
this.flowEngine.registerActions(actions);
|
||||
const dataSourceManager = new DataSourceManager();
|
||||
this.flowEngine.context['app'] = this.app;
|
||||
this.flowEngine.context['api'] = this.app.apiClient;
|
||||
this.flowEngine.context['flowEngine'] = this.flowEngine;
|
||||
this.flowEngine.context['dataSourceManager'] = dataSourceManager;
|
||||
try {
|
||||
const response = await this.app.apiClient.request<any>({
|
||||
url: '/collections:listMeta',
|
||||
});
|
||||
const mainDataSource = new DataSource({
|
||||
name: 'main',
|
||||
key: 'main',
|
||||
displayName: 'Main',
|
||||
});
|
||||
dataSourceManager.addDataSource(mainDataSource);
|
||||
const collections = response.data?.data || [];
|
||||
collections.forEach((collection) => {
|
||||
mainDataSource.addCollection(collection);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load collections:', error);
|
||||
// Optionally, you can throw an error or handle it as needed
|
||||
}
|
||||
this.app.addProvider(FlowEngineRunner, {});
|
||||
// 注册通用 flow
|
||||
this.flowEngine.registerAction(DateTimeFormat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,58 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FlowModel } from '@nocobase/flow-engine';
|
||||
import { Modal } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
export class ActionModel extends FlowModel {
|
||||
set onClick(fn) {
|
||||
this.setProps('onClick', fn);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a {...this.props}>{this.props.title || 'Untitle'}</a>;
|
||||
}
|
||||
}
|
||||
|
||||
ActionModel.registerFlow({
|
||||
key: 'default',
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
ctx.model.setProps('title', params.title);
|
||||
ctx.model.onClick = (e) => {
|
||||
ctx.model.dispatchEvent('click', {
|
||||
event: e,
|
||||
record: ctx.extra.record,
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ActionModel.registerFlow({
|
||||
key: 'event1',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
Modal.confirm({
|
||||
title: `${ctx.extra.record?.id}`,
|
||||
content: 'Are you sure you want to perform this action?',
|
||||
onOk: async () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -1,127 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FormItem, Input } from '@formily/antd-v5';
|
||||
import { Field, Form } from '@formily/core';
|
||||
import { FieldContext } from '@formily/react';
|
||||
import { CollectionField, FlowModel } from '@nocobase/flow-engine';
|
||||
import React from 'react';
|
||||
import { ReactiveField } from '../Formily/ReactiveField';
|
||||
|
||||
export class FormFieldModel extends FlowModel {
|
||||
collectionField: CollectionField;
|
||||
field: Field;
|
||||
|
||||
get form() {
|
||||
return this.parent.form as Form;
|
||||
}
|
||||
|
||||
setTitle(title: string) {
|
||||
this.field.title = title || this.collectionField.title;
|
||||
}
|
||||
|
||||
setRequired(required: boolean) {
|
||||
this.field.required = required;
|
||||
}
|
||||
|
||||
setInitialValue(initialValue: any) {
|
||||
this.field.initialValue = initialValue;
|
||||
}
|
||||
|
||||
createField() {
|
||||
return this.form.createField({
|
||||
name: this.collectionField.name,
|
||||
...this.props,
|
||||
decorator: [
|
||||
FormItem,
|
||||
{
|
||||
title: this.props.title,
|
||||
},
|
||||
],
|
||||
component: [Input, {}],
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FieldContext.Provider value={this.field}>
|
||||
<ReactiveField field={this.field}>{this.props.children}</ReactiveField>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormFieldModel.registerFlow({
|
||||
key: 'default',
|
||||
auto: true,
|
||||
title: 'Basic',
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
const collectionField = ctx.globals.dataSourceManager.getCollectionField(params.fieldPath);
|
||||
ctx.model.collectionField = collectionField;
|
||||
ctx.model.field = ctx.model.createField();
|
||||
},
|
||||
},
|
||||
editTitle: {
|
||||
title: 'Edit Title',
|
||||
uiSchema: {
|
||||
title: {
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
placeholder: 'Enter field title',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setTitle(params.title);
|
||||
},
|
||||
},
|
||||
initialValue: {
|
||||
title: 'Default value',
|
||||
uiSchema: {
|
||||
defaultValue: {
|
||||
'x-component': 'Input',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {},
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setInitialValue(params.defaultValue);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export class CommonFormItemFlowModel extends FormFieldModel {}
|
||||
|
||||
FormFieldModel.registerFlow({
|
||||
key: 'key2',
|
||||
auto: true,
|
||||
title: 'Group2',
|
||||
steps: {
|
||||
required: {
|
||||
title: 'Required',
|
||||
uiSchema: {
|
||||
required: {
|
||||
'x-component': 'Switch',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
checkedChildren: 'Yes',
|
||||
unCheckedChildren: 'No',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler(ctx, params) {
|
||||
ctx.model.setRequired(params.required || false);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -1,133 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FormButtonGroup, FormDialog, FormLayout, Submit } from '@formily/antd-v5';
|
||||
import { createForm, Form } from '@formily/core';
|
||||
import { FormProvider } from '@formily/react';
|
||||
import {
|
||||
AddActionButton,
|
||||
AddFieldButton,
|
||||
AddFieldButtonProps,
|
||||
Collection,
|
||||
FlowEngineProvider,
|
||||
FlowModelRenderer,
|
||||
SingleRecordResource,
|
||||
} from '@nocobase/flow-engine';
|
||||
import { Card } from 'antd';
|
||||
import React from 'react';
|
||||
import { ActionModel } from './ActionModel';
|
||||
import { BlockFlowModel } from './BlockFlowModel';
|
||||
import { FormFieldModel } from './FormFieldModel';
|
||||
|
||||
export class FormModel extends BlockFlowModel {
|
||||
form: Form;
|
||||
resource: SingleRecordResource;
|
||||
collection: Collection;
|
||||
|
||||
render() {
|
||||
const buildColumnSubModelParams: AddFieldButtonProps['buildSubModelParams'] = (item) => {
|
||||
return {
|
||||
use: 'FormFieldModel',
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
fieldPath: `${item.field.collection.dataSource.name}.${item.field.collection.name}.${item.field.name}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
return (
|
||||
<Card>
|
||||
<FormProvider form={this.form}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
{this.mapSubModels('fields', (field) => (
|
||||
<FlowModelRenderer model={field} showFlowSettings />
|
||||
))}
|
||||
</FormLayout>
|
||||
<AddFieldButton
|
||||
buildSubModelParams={buildColumnSubModelParams}
|
||||
onModelAdded={async (fieldModel: FormFieldModel, item) => {
|
||||
fieldModel.collectionField = item.field;
|
||||
}}
|
||||
subModelKey="fields"
|
||||
model={this}
|
||||
collection={this.collection}
|
||||
ParentModelClass={FormFieldModel}
|
||||
/>
|
||||
<FormButtonGroup>
|
||||
{this.mapSubModels('actions', (action) => (
|
||||
<FlowModelRenderer model={action} />
|
||||
))}
|
||||
<AddActionButton model={this} ParentModelClass={ActionModel} />
|
||||
</FormButtonGroup>
|
||||
</FormProvider>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FormModel.registerFlow({
|
||||
key: 'default',
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
paramsRequired: true,
|
||||
hideInSettings: true,
|
||||
uiSchema: {
|
||||
dataSourceKey: {
|
||||
type: 'string',
|
||||
title: 'Data Source Key',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: 'Enter data source key',
|
||||
},
|
||||
},
|
||||
collectionName: {
|
||||
type: 'string',
|
||||
title: 'Collection Name',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: 'Enter collection name',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
dataSourceKey: 'main',
|
||||
},
|
||||
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);
|
||||
const resource = new SingleRecordResource();
|
||||
resource.setDataSourceKey(params.dataSourceKey);
|
||||
resource.setResourceName(params.collectionName);
|
||||
resource.setAPIClient(ctx.globals.api);
|
||||
ctx.model.resource = resource;
|
||||
if (ctx.extra.filterByTk) {
|
||||
resource.setFilterByTk(ctx.extra.filterByTk);
|
||||
await resource.refresh();
|
||||
ctx.model.form.setInitialValues(resource.getData());
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
FormModel.define({
|
||||
title: 'Form',
|
||||
group: 'Content',
|
||||
defaultOptions: {
|
||||
use: 'FormModel',
|
||||
},
|
||||
});
|
@ -1,116 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FormButtonGroup, FormDialog, FormLayout, Submit } from '@formily/antd-v5';
|
||||
import { createForm, Form } from '@formily/core';
|
||||
import { FormProvider } from '@formily/react';
|
||||
import {
|
||||
Collection,
|
||||
CollectionField,
|
||||
FlowEngine,
|
||||
FlowEngineProvider,
|
||||
FlowModelRenderer,
|
||||
SingleRecordResource,
|
||||
} from '@nocobase/flow-engine';
|
||||
import dataSource from 'packages/core/client/docs/zh-CN/core/flow-models/demos/data-source';
|
||||
import React from 'react';
|
||||
import { BlockFlowModel } from './BlockFlowModel';
|
||||
|
||||
export class QuickEditForm extends BlockFlowModel {
|
||||
form: Form;
|
||||
resource: SingleRecordResource;
|
||||
collection: Collection;
|
||||
|
||||
static async open(options: { flowEngine: FlowEngine; collectionField: CollectionField; filterByTk: string }) {
|
||||
const model = options.flowEngine.createModel({
|
||||
use: 'QuickEditForm',
|
||||
}) as QuickEditForm;
|
||||
await model.open(options);
|
||||
options.flowEngine.removeModel(model.uid);
|
||||
}
|
||||
|
||||
async open({ collectionField, filterByTk }: { filterByTk: string; collectionField: CollectionField }) {
|
||||
await this.applyFlow('initial', {
|
||||
dataSourceKey: collectionField.collection.dataSource.name,
|
||||
collectionName: collectionField.collection.name,
|
||||
filterByTk,
|
||||
fieldPath: collectionField.fullpath,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
const dialog = FormDialog(
|
||||
{
|
||||
footer: null,
|
||||
title: 'Quick edit',
|
||||
},
|
||||
(form) => {
|
||||
return (
|
||||
<FlowEngineProvider engine={this.flowEngine}>
|
||||
<FormProvider form={this.form}>
|
||||
<FormLayout layout={'vertical'}>
|
||||
{this.mapSubModels('fields', (field) => (
|
||||
<FlowModelRenderer model={field} />
|
||||
))}
|
||||
</FormLayout>
|
||||
<FormButtonGroup>
|
||||
<Submit
|
||||
onClick={async () => {
|
||||
await this.resource.save(this.form.values);
|
||||
dialog.close();
|
||||
resolve(this.form.values); // 在 close 之后 resolve
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Submit>
|
||||
</FormButtonGroup>
|
||||
</FormProvider>
|
||||
</FlowEngineProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
dialog.open();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QuickEditForm.registerFlow({
|
||||
key: 'initial',
|
||||
steps: {
|
||||
step1: {
|
||||
async handler(ctx) {
|
||||
ctx.model.form = createForm();
|
||||
ctx.model.collection = ctx.globals.dataSourceManager.getCollection(
|
||||
ctx.extra.dataSourceKey,
|
||||
ctx.extra.collectionName,
|
||||
);
|
||||
const resource = new SingleRecordResource();
|
||||
resource.setDataSourceKey(ctx.extra.dataSourceKey);
|
||||
resource.setResourceName(ctx.extra.collectionName);
|
||||
resource.setAPIClient(ctx.globals.api);
|
||||
ctx.model.resource = resource;
|
||||
if (ctx.extra.filterByTk) {
|
||||
resource.setFilterByTk(ctx.extra.filterByTk);
|
||||
await resource.refresh();
|
||||
ctx.model.form.setInitialValues(resource.getData());
|
||||
}
|
||||
if (ctx.extra.fieldPath) {
|
||||
ctx.model.addSubModel('fields', {
|
||||
use: 'FormItemModel',
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
fieldPath: ctx.extra.fieldPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { Submit } from '@formily/antd-v5';
|
||||
import React from 'react';
|
||||
import { ActionModel } from './ActionModel';
|
||||
|
||||
export class SubmitActionModel extends ActionModel {
|
||||
render() {
|
||||
return <Submit {...this.props}>{this.props.title || 'Submit'}</Submit>;
|
||||
}
|
||||
}
|
||||
|
||||
SubmitActionModel.registerFlow({
|
||||
key: 'event1',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -1,133 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { CollectionField, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine';
|
||||
import { Space, TableColumnProps, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
import { ActionModel } from './ActionModel';
|
||||
import { FieldFlowModel } from './FieldFlowModel';
|
||||
import { QuickEditForm } from './QuickEditForm';
|
||||
|
||||
export class TableColumnModel extends FieldFlowModel {
|
||||
// field: Field;
|
||||
// fieldPath: string;
|
||||
|
||||
getColumnProps(): TableColumnProps {
|
||||
return {
|
||||
...this.props,
|
||||
title: (
|
||||
<FlowsFloatContextMenu
|
||||
model={this}
|
||||
containerStyle={{ display: 'block', padding: '11px 8px', margin: '-11px -8px' }}
|
||||
>
|
||||
{this.props.title}
|
||||
</FlowsFloatContextMenu>
|
||||
),
|
||||
ellipsis: true,
|
||||
onCell: (record) => ({
|
||||
className: css`
|
||||
.edit-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
color: #1890ff;
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
&:hover {
|
||||
background: rgba(24, 144, 255, 0.1) !important;
|
||||
}
|
||||
&:hover .edit-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
`,
|
||||
}),
|
||||
render: this.render(),
|
||||
};
|
||||
}
|
||||
|
||||
renderQuickEditButton(record) {
|
||||
return (
|
||||
<Tooltip title="快速编辑">
|
||||
<EditOutlined
|
||||
className="edit-icon"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await QuickEditForm.open({
|
||||
flowEngine: this.flowEngine,
|
||||
collectionField: this.field as CollectionField,
|
||||
filterByTk: record.id,
|
||||
});
|
||||
await this.parent.resource.refresh();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (value, record, index) => (
|
||||
<>
|
||||
{value}
|
||||
{this.renderQuickEditButton(record)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableColumnModel.define({
|
||||
title: 'Table Column',
|
||||
icon: 'TableColumn',
|
||||
defaultOptions: {
|
||||
use: 'TableColumnModel',
|
||||
},
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
export class TableColumnActionsModel extends TableColumnModel {
|
||||
getColumnProps() {
|
||||
return { title: 'Actions', ...this.props, render: this.render() };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (value, record, index) => (
|
||||
<Space>
|
||||
{this.mapSubModels('actions', (action: ActionModel) => (
|
||||
<FlowModelRenderer key={action.uid} model={action} extraContext={{ record }} />
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableColumnModel.registerFlow({
|
||||
key: 'default',
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
if (!params.fieldPath) {
|
||||
return;
|
||||
}
|
||||
if (ctx.model.field) {
|
||||
return;
|
||||
}
|
||||
const field = ctx.globals.dataSourceManager.getCollectionField(params.fieldPath);
|
||||
ctx.model.fieldPath = params.fieldPath;
|
||||
ctx.model.setProps('title', field.title);
|
||||
ctx.model.setProps('dataIndex', field.name);
|
||||
ctx.model.field = field;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -1,148 +0,0 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import {
|
||||
AddActionButton,
|
||||
AddFieldButton,
|
||||
AddFieldButtonProps,
|
||||
AddFieldMenuItem,
|
||||
AddSubModelMenuItem,
|
||||
Collection,
|
||||
FlowModel,
|
||||
MultiRecordResource,
|
||||
} from '@nocobase/flow-engine';
|
||||
import { Button, Card, Dropdown, Table } from 'antd';
|
||||
import React from 'react';
|
||||
import { BlockFlowModel } from './BlockFlowModel';
|
||||
import { FieldFlowModel } from './FieldFlowModel';
|
||||
import { TableColumnModel } from './TableColumnModel';
|
||||
|
||||
type S = {
|
||||
subModels: {
|
||||
columns: TableColumnModel[];
|
||||
};
|
||||
};
|
||||
|
||||
export class TableModel extends BlockFlowModel<S> {
|
||||
collection: Collection;
|
||||
resource: MultiRecordResource;
|
||||
|
||||
getColumns() {
|
||||
const buildColumnSubModelParams: AddFieldButtonProps['buildSubModelParams'] = (item) => {
|
||||
return {
|
||||
use: 'TableColumnModel',
|
||||
props: {
|
||||
dataIndex: item.field.name,
|
||||
title: item.field.title,
|
||||
},
|
||||
};
|
||||
};
|
||||
const onModelAdded = async (column: TableColumnModel, item: AddFieldMenuItem) => {
|
||||
const field = item.field;
|
||||
column.field = field;
|
||||
column.fieldPath = `${field.collection.dataSource.name}.${field.collection.name}.${field.name}`;
|
||||
column.setStepParams('default', 'step1', {
|
||||
fieldPath: column.fieldPath,
|
||||
});
|
||||
await column.applyAutoFlows();
|
||||
};
|
||||
return this.mapSubModels('columns', (column) => {
|
||||
const ps = column.getColumnProps();
|
||||
return ps;
|
||||
}).concat({
|
||||
key: 'addColumn',
|
||||
fixed: 'right',
|
||||
title: (
|
||||
<AddFieldButton
|
||||
onModelAdded={onModelAdded}
|
||||
buildSubModelParams={buildColumnSubModelParams}
|
||||
subModelKey="columns"
|
||||
model={this}
|
||||
collection={this.collection}
|
||||
ParentModelClass={FieldFlowModel}
|
||||
/>
|
||||
),
|
||||
} as any);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card>
|
||||
<Table
|
||||
rowKey="id"
|
||||
dataSource={this.resource.getData()}
|
||||
columns={this.getColumns()}
|
||||
pagination={{
|
||||
current: this.resource.getMeta('page'),
|
||||
pageSize: this.resource.getMeta('pageSize'),
|
||||
total: this.resource.getMeta('count'),
|
||||
}}
|
||||
onChange={(pagination) => {
|
||||
this.resource.setPage(pagination.current);
|
||||
this.resource.setPageSize(pagination.pageSize);
|
||||
this.resource.refresh();
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableModel.registerFlow({
|
||||
key: 'default',
|
||||
auto: true,
|
||||
steps: {
|
||||
step1: {
|
||||
paramsRequired: true,
|
||||
hideInSettings: true,
|
||||
uiSchema: {
|
||||
dataSourceKey: {
|
||||
type: 'string',
|
||||
title: 'Data Source Key',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: 'Enter data source key',
|
||||
},
|
||||
},
|
||||
collectionName: {
|
||||
type: 'string',
|
||||
title: 'Collection Name',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: 'Enter collection name',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultParams: {
|
||||
dataSourceKey: 'main',
|
||||
},
|
||||
handler: async (ctx, params) => {
|
||||
const collection = ctx.globals.dataSourceManager.getCollection(params.dataSourceKey, params.collectionName);
|
||||
ctx.model.collection = collection;
|
||||
const resource = new MultiRecordResource();
|
||||
resource.setDataSourceKey(params.dataSourceKey);
|
||||
resource.setResourceName(params.collectionName);
|
||||
resource.setAPIClient(ctx.globals.api);
|
||||
ctx.model.resource = resource;
|
||||
await resource.refresh();
|
||||
await ctx.model.applySubModelsAutoFlows('columns');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
TableModel.define({
|
||||
title: 'Table',
|
||||
group: 'Content',
|
||||
defaultOptions: {
|
||||
use: 'TableModel',
|
||||
},
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { ButtonProps } from 'antd';
|
||||
import { GlobalActionModel } from '../base/ActionModel';
|
||||
|
||||
export class AddNewActionModel extends GlobalActionModel {
|
||||
defaultProps: ButtonProps = {
|
||||
type: 'primary',
|
||||
title: 'Add new',
|
||||
icon: 'PlusOutlined',
|
||||
};
|
||||
}
|
||||
|
||||
AddNewActionModel.registerFlow({
|
||||
sort: 200,
|
||||
title: '点击事件',
|
||||
key: 'handleClick',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
popup: {
|
||||
use: 'popup',
|
||||
defaultParams(ctx) {
|
||||
return {
|
||||
sharedContext: {
|
||||
parentBlockModel: ctx.shared?.currentBlockModel,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 { MultiRecordResource } from '@nocobase/flow-engine';
|
||||
import { ButtonProps } from 'antd';
|
||||
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
|
||||
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
|
||||
import { GlobalActionModel } from '../base/ActionModel';
|
||||
|
||||
export class BulkDeleteActionModel extends GlobalActionModel {
|
||||
defaultProps: ButtonProps = {
|
||||
title: 'Delete',
|
||||
icon: 'DeleteOutlined',
|
||||
};
|
||||
}
|
||||
|
||||
BulkDeleteActionModel.registerFlow({
|
||||
key: 'handleClick',
|
||||
title: '点击事件',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
confirm: {
|
||||
use: 'confirm',
|
||||
},
|
||||
delete: {
|
||||
async handler(ctx, params) {
|
||||
if (!ctx.shared?.currentBlockModel?.resource) {
|
||||
ctx.globals.message.error('No resource selected for deletion.');
|
||||
return;
|
||||
}
|
||||
const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource;
|
||||
if (resource.getSelectedRows().length === 0) {
|
||||
ctx.globals.message.warning('No records selected for deletion.');
|
||||
return;
|
||||
}
|
||||
await resource.destroySelectedRows();
|
||||
ctx.globals.message.success('Selected records deleted successfully.');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user