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
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./docker/nocobase
|
context: ./docker/nocobase
|
||||||
file: ./docker/nocobase/Dockerfile-cn
|
file: ./docker/nocobase/Dockerfile-full
|
||||||
build-args: |
|
build-args: |
|
||||||
CNA_VERSION=${{ inputs.tag_name }}
|
CNA_VERSION=${{ inputs.tag_name }}
|
||||||
platforms: linux/amd64,linux/arm64
|
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
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./docker/nocobase
|
context: ./docker/nocobase
|
||||||
file: ./docker/nocobase/Dockerfile-cn
|
file: ./docker/nocobase/Dockerfile-full
|
||||||
build-args: |
|
build-args: |
|
||||||
CNA_VERSION=${{ steps.get-info.outputs.defaultTag }}
|
CNA_VERSION=${{ steps.get-info.outputs.defaultTag }}
|
||||||
platforms: linux/amd64,linux/arm64
|
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/)
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [v1.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
|
## [v1.7.9](https://github.com/nocobase/nocobase/compare/v1.7.8...v1.7.9) - 2025-06-11
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
@ -5,6 +5,148 @@
|
|||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||||
|
|
||||||
|
## [v1.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
|
## [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",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": ["--ignore-engines"],
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/acl",
|
"name": "@nocobase/acl",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/resourcer": "1.8.0-alpha.5",
|
"@nocobase/resourcer": "1.8.0-alpha.9",
|
||||||
"@nocobase/utils": "1.8.0-alpha.5",
|
"@nocobase/utils": "1.8.0-alpha.9",
|
||||||
"minimatch": "^5.1.1"
|
"minimatch": "^5.1.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/actions",
|
"name": "@nocobase/actions",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/cache": "1.8.0-alpha.5",
|
"@nocobase/cache": "1.8.0-alpha.9",
|
||||||
"@nocobase/database": "1.8.0-alpha.5",
|
"@nocobase/database": "1.8.0-alpha.9",
|
||||||
"@nocobase/resourcer": "1.8.0-alpha.5"
|
"@nocobase/resourcer": "1.8.0-alpha.9"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -9,4 +9,7 @@
|
|||||||
|
|
||||||
import { proxyToRepository } from './proxy-to-repository';
|
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';
|
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",
|
"name": "@nocobase/app",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/database": "1.8.0-alpha.5",
|
"@nocobase/database": "1.8.0-alpha.9",
|
||||||
"@nocobase/preset-nocobase": "1.8.0-alpha.5",
|
"@nocobase/preset-nocobase": "1.8.0-alpha.9",
|
||||||
"@nocobase/server": "1.8.0-alpha.5"
|
"@nocobase/server": "1.8.0-alpha.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/client": "1.8.0-alpha.5"
|
"@nocobase/client": "1.8.0-alpha.9"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/auth",
|
"name": "@nocobase/auth",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/actions": "1.8.0-alpha.5",
|
"@nocobase/actions": "1.8.0-alpha.9",
|
||||||
"@nocobase/cache": "1.8.0-alpha.5",
|
"@nocobase/cache": "1.8.0-alpha.9",
|
||||||
"@nocobase/database": "1.8.0-alpha.5",
|
"@nocobase/database": "1.8.0-alpha.9",
|
||||||
"@nocobase/resourcer": "1.8.0-alpha.5",
|
"@nocobase/resourcer": "1.8.0-alpha.9",
|
||||||
"@nocobase/utils": "1.8.0-alpha.5",
|
"@nocobase/utils": "1.8.0-alpha.9",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/build",
|
"name": "@nocobase/build",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "Library build tool based on rollup.",
|
"description": "Library build tool based on rollup.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
|
4
packages/core/cache/package.json
vendored
4
packages/core/cache/package.json
vendored
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@nocobase/cache",
|
"name": "@nocobase/cache",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/lock-manager": "1.8.0-alpha.5",
|
"@nocobase/lock-manager": "1.8.0-alpha.9",
|
||||||
"bloom-filters": "^3.0.1",
|
"bloom-filters": "^3.0.1",
|
||||||
"cache-manager": "^5.2.4",
|
"cache-manager": "^5.2.4",
|
||||||
"cache-manager-redis-yet": "^4.1.2"
|
"cache-manager-redis-yet": "^4.1.2"
|
||||||
|
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(val2).toBe(obj);
|
||||||
expect(await cache.get('key')).toMatchObject(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",
|
"name": "@nocobase/cli",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "./src/index.js",
|
"main": "./src/index.js",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"nocobase": "./bin/index.js"
|
"nocobase": "./bin/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nocobase/app": "1.8.0-alpha.5",
|
"@nocobase/app": "1.8.0-alpha.9",
|
||||||
"@nocobase/license-kit": "^0.2.3",
|
"@nocobase/license-kit": "^0.2.3",
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@umijs/utils": "3.5.20",
|
"@umijs/utils": "3.5.20",
|
||||||
@ -27,7 +27,7 @@
|
|||||||
"tsx": "^4.19.0"
|
"tsx": "^4.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nocobase/devtools": "1.8.0-alpha.5"
|
"@nocobase/devtools": "1.8.0-alpha.9"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -115,7 +115,7 @@ exports.postCheck = async (opts) => {
|
|||||||
const port = opts.port || process.env.APP_PORT;
|
const port = opts.port || process.env.APP_PORT;
|
||||||
const result = await exports.isPortReachable(port);
|
const result = await exports.isPortReachable(port);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.error(chalk.red(`post already in use ${port}`));
|
console.error(chalk.red(`Port ${port} already in use`));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@ console.log('process.env.DOC_LANG', lang);
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
hash: true,
|
hash: true,
|
||||||
|
mfsu:false,
|
||||||
alias: {
|
alias: {
|
||||||
...umiConfig.alias,
|
...umiConfig.alias,
|
||||||
},
|
},
|
||||||
@ -82,6 +83,10 @@ export default defineConfig({
|
|||||||
// },
|
// },
|
||||||
// ],
|
// ],
|
||||||
// },
|
// },
|
||||||
|
{
|
||||||
|
title: 'Quickstart',
|
||||||
|
link: '/core/flow-models/quickstart',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'FlowEngine',
|
title: 'FlowEngine',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
@ -140,10 +145,6 @@ export default defineConfig({
|
|||||||
title: 'Flow Models',
|
title: 'Flow Models',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
title: 'Quickstart',
|
|
||||||
link: '/core/flow-models/quickstart',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Overview',
|
title: 'Overview',
|
||||||
link: '/core/flow-models',
|
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
|
# 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> {
|
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||||
// 从本地存储加载模型数据
|
// 从本地存储加载模型数据
|
||||||
async load(uid: string) {
|
async findOne({ uid }) {
|
||||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# FlowAction
|
# FlowAction
|
||||||
|
|
||||||
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作(Action)的核心对象。每个操作(Action)封装一段可执行的业务逻辑,可以在多个流步骤中复用,支持参数配置、UI 配置和类型推断。
|
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作(Action)的核心对象。每个操作封装一段可执行的业务逻辑,可在多个流步骤中复用,并支持参数配置、UI 配置和类型推断。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -10,48 +10,50 @@
|
|||||||
interface ActionDefinition {
|
interface ActionDefinition {
|
||||||
name: string; // 操作唯一标识,必须唯一
|
name: string; // 操作唯一标识,必须唯一
|
||||||
title?: string; // 操作显示名称(可选)
|
title?: string; // 操作显示名称(可选)
|
||||||
uiSchema?: Record<string, ISchema>; // (可选)用于参数配置界面渲染
|
uiSchema?: Record<string, ISchema>; // (可选)参数配置界面渲染
|
||||||
defaultParams?: Record<string, any>; // (可选)默认参数
|
defaultParams?: Record<string, any>; // (可选)默认参数
|
||||||
paramsRequired?: boolean; // (可选)是否需要参数配置,为true时添加模型前会打开配置对话框
|
paramsRequired?: boolean; // (可选)是否需要参数配置
|
||||||
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏该步骤
|
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏
|
||||||
handler: (ctx: FlowContext, params: any) => Promise<any> | any; // 操作执行逻辑
|
handler: (ctx: FlowContext, params: any) => Promise<any> | any; // 操作执行逻辑
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 定义操作的方式
|
## 使用说明
|
||||||
|
|
||||||
### 1. 使用 defineAction 工具函数
|
### 1. 定义 Action
|
||||||
|
|
||||||
推荐方式,结构清晰、类型推断友好:
|
#### 方式一:使用 defineAction 工具函数(推荐)
|
||||||
|
|
||||||
|
结构清晰,类型推断友好:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const myAction = defineAction({
|
const myAction = defineAction({
|
||||||
name: 'actionName',
|
name: 'myAction',
|
||||||
title: '操作显示名称',
|
title: '操作显示名称',
|
||||||
uiSchema: {},
|
uiSchema: {},
|
||||||
defaultParams: {},
|
defaultParams: {},
|
||||||
paramsRequired: true, // 添加模型前强制打开配置对话框
|
paramsRequired: true,
|
||||||
hideInSettings: false, // 在设置菜单中显示
|
hideInSettings: false,
|
||||||
async handler(ctx, params) {
|
async handler(ctx, params) {
|
||||||
// 操作逻辑
|
// 操作逻辑
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 实现 ActionDefinition 接口
|
#### 方式二:实现 ActionDefinition 接口
|
||||||
|
|
||||||
适合需要扩展属性或方法的场景:
|
复杂场景时可以通过定义 Action 类来处理更复杂的操作
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
class MyAction implements ActionDefinition {
|
class MyAction implements ActionDefinition {
|
||||||
name = 'actionName';
|
name = 'myAction';
|
||||||
title = '操作显示名称';
|
title = '操作显示名称';
|
||||||
uiSchema = {};
|
uiSchema = {};
|
||||||
defaultParams = {};
|
defaultParams = {};
|
||||||
paramsRequired = true; // 添加模型前强制打开配置对话框
|
paramsRequired = true;
|
||||||
hideInSettings = false; // 在设置菜单中显示
|
hideInSettings = false;
|
||||||
async handler(ctx, params) {
|
async handler(ctx, params) {
|
||||||
// 操作逻辑
|
// 操作逻辑
|
||||||
}
|
}
|
||||||
@ -60,59 +62,115 @@ class MyAction implements ActionDefinition {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 注册操作
|
### 2. 注册到 FlowEngine 里
|
||||||
|
|
||||||
注册后可在流步骤中通过 `use` 字段复用:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
flowEngine.registerAction({
|
|
||||||
name: 'actionName',
|
|
||||||
title: '操作显示名称',
|
|
||||||
uiSchema: {},
|
|
||||||
defaultParams: {},
|
|
||||||
paramsRequired: true, // 添加模型前强制打开配置对话框
|
|
||||||
hideInSettings: false, // 在设置菜单中显示
|
|
||||||
handler(ctx, params) {
|
|
||||||
// 操作逻辑
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
flowEngine.registerAction(myAction); // 注册 defineAction 返回的对象
|
flowEngine.registerAction(myAction); // 注册 defineAction 返回的对象
|
||||||
flowEngine.registerAction(new MyAction()); // 注册类实例
|
flowEngine.registerAction(new MyAction()); // 注册类实例
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 在流中复用操作
|
### 3. 在流中使用
|
||||||
|
|
||||||
在流步骤定义中通过 `use` 字段引用已注册的操作:
|
在流步骤定义中通过 `use` 字段引用已注册的操作:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
use: 'actionName', // 复用已注册的操作
|
use: 'myAction', // 复用已注册的操作
|
||||||
defaultParams: {},
|
defaultParams: {},
|
||||||
paramsRequired: true, // 可以在步骤级别覆盖操作的paramsRequired设置
|
paramsRequired: true, // 可覆盖操作的 paramsRequired
|
||||||
hideInSettings: false, // 可以在步骤级别覆盖操作的hideInSettings设置
|
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
|
### paramsRequired
|
||||||
|
|
||||||
- **类型**: `boolean`
|
- **类型**: `boolean`
|
||||||
- **默认值**: `false`
|
- **默认值**: `false`
|
||||||
- **说明**: 当设置为 `true` 时,在添加该步骤模型前会强制打开参数配置对话框,确保用户配置必要的参数。适用于需要用户必须配置参数才能正常工作的操作。
|
- **说明**: 为 `true` 时,添加步骤前会强制打开参数配置对话框,确保用户配置必要参数。适用于参数必填的场景。
|
||||||
|
|
||||||
### hideInSettings
|
### hideInSettings
|
||||||
|
|
||||||
- **类型**: `boolean`
|
- **类型**: `boolean`
|
||||||
- **默认值**: `false`
|
- **默认值**: `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** 让流步骤逻辑高度复用,便于维护和扩展。
|
- **FlowAction** 让流步骤逻辑高度复用,便于维护和扩展。
|
||||||
- 支持多种定义方式,适应不同复杂度的业务场景。
|
- 支持多种定义方式,适应不同复杂度的业务场景。
|
||||||
- 可通过 `uiSchema` 和 `defaultParams` 配置参数界面和默认值,提升易用性。
|
- 可通过 `uiSchema` 和 `defaultParams` 配置参数界面和默认值,提升易用性。
|
||||||
|
- 合理使用 `paramsRequired` 和 `hideInSettings`,提升操作安全性和灵活性。
|
||||||
|
@ -7,18 +7,34 @@
|
|||||||
## 核心结构
|
## 核心结构
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface FlowDefinition {
|
interface FlowDefinition<TModel extends FlowModel = FlowModel> {
|
||||||
key: string; // 流唯一标识
|
key: string; // 流唯一标识
|
||||||
on?: { event: string }; // 可选:事件触发配置
|
title?: string; // 可选:流显示名称
|
||||||
auto?: boolean; // 可选:是否自动运行
|
auto?: boolean; // 可选:是否自动运行
|
||||||
steps: Record<string, StepDefinition>; // 流步骤定义
|
sort?: number; // 可选:流执行排序,数字越小越先执行,默认为 0,可为负数
|
||||||
|
on?: { eventName: string }; // 可选:事件触发配置
|
||||||
|
steps: Record<string, StepDefinition<TModel>>; // 流步骤定义
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StepDefinition {
|
// 步骤定义支持两种类型:ActionStepDefinition 和 InlineStepDefinition
|
||||||
use?: string; // 可选:引用已注册的全局 Action
|
interface ActionStepDefinition<TModel extends FlowModel = FlowModel> {
|
||||||
defaultParams?: any; // 默认参数
|
use: string; // 引用已注册的全局 Action 名称
|
||||||
uiSchema?: any; // 可选:用于 FlowSettings 配置界面
|
title?: string; // 可选:步骤显示名称
|
||||||
handler?: (ctx: any, params: any) => Promise<any>; // 可选:步骤处理函数
|
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>({
|
const myFlow = defineFlow<MyFlowSteps>({
|
||||||
key: 'myFlow',
|
key: 'myFlow',
|
||||||
on: { event: 'user.created' }, // 监听 user.created 事件自动触发
|
title: '我的流程',
|
||||||
|
auto: true, // 自动执行
|
||||||
|
sort: 100, // 执行顺序
|
||||||
|
on: { eventName: 'user.created' }, // 监听 user.created 事件自动触发
|
||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
defaultParams: {},
|
title: '步骤1',
|
||||||
|
// 静态默认参数
|
||||||
|
defaultParams: {
|
||||||
|
name: 'test'
|
||||||
|
},
|
||||||
async handler(ctx, params) {
|
async handler(ctx, params) {
|
||||||
// 步骤 1 的处理逻辑
|
// 步骤 1 的处理逻辑
|
||||||
|
ctx.logger.info('执行步骤1', params);
|
||||||
// 例如:console.log(params.name);
|
// 例如:console.log(params.name);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
step2: {
|
step2: {
|
||||||
uiSchema: {}, // 可用于 UI 配置
|
title: '步骤2',
|
||||||
defaultParams: {},
|
uiSchema: {
|
||||||
|
age: {
|
||||||
|
type: 'number',
|
||||||
|
title: '年龄',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
}
|
||||||
|
}, // 可用于 UI 配置
|
||||||
|
// 动态默认参数 - 根据模型状态生成
|
||||||
|
defaultParams: (ctx) => ({
|
||||||
|
name: ctx.model.name,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}),
|
||||||
async handler(ctx, params) {
|
async handler(ctx, params) {
|
||||||
// 步骤 2 的处理逻辑
|
// 步骤 2 的处理逻辑
|
||||||
|
ctx.logger.info('执行步骤2', params);
|
||||||
|
// 可以访问前一步的结果:ctx.stepResults.step1
|
||||||
// 例如:console.log(params.age);
|
// 例如:console.log(params.age);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -70,6 +107,9 @@ MyFlowModel.registerFlow(myFlow); // 注册流
|
|||||||
```ts
|
```ts
|
||||||
class MyFlowDefinition implements FlowDefinition {
|
class MyFlowDefinition implements FlowDefinition {
|
||||||
key = 'MyFlowDefinition';
|
key = 'MyFlowDefinition';
|
||||||
|
title = '我的复杂流程';
|
||||||
|
auto = true;
|
||||||
|
sort = 0;
|
||||||
|
|
||||||
steps = {
|
steps = {
|
||||||
step1: {
|
step1: {
|
||||||
@ -156,8 +196,8 @@ myModel.setStepParams('myFlow', 'step1', { name: '小明' });
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
await myModel.applyFlow('myFlow'); // 主动执行指定流
|
await myModel.applyFlow('myFlow'); // 主动执行指定流
|
||||||
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.event)
|
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.eventName)
|
||||||
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流,按 sort 排序
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -169,8 +209,10 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
|||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| ----------- | -------------------------------- | ---------------------------------- |
|
| ----------- | -------------------------------- | ---------------------------------- |
|
||||||
| `key` | `string` | 流唯一标识,必须配置 |
|
| `key` | `string` | 流唯一标识,必须配置 |
|
||||||
| `on` | `{ event: string }` | (可选)事件触发配置 |
|
| `title` | `string` | (可选)流显示名称 |
|
||||||
|
| `on` | `{ eventName: string }` | (可选)事件触发配置 |
|
||||||
| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 |
|
| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 |
|
||||||
|
| `sort` | `number` | (可选)流执行排序,数字越小越先执行,默认为 0,可为负数 |
|
||||||
| `steps` | `Record<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
|
| `steps` | `Record<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
|
||||||
|
|
||||||
### StepDefinition 配置速查表
|
### StepDefinition 配置速查表
|
||||||
@ -178,8 +220,12 @@ await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
|
|||||||
| 字段 | 类型 | 说明 |
|
| 字段 | 类型 | 说明 |
|
||||||
| --------------- | -------------------------------------- | ------------------------------------- |
|
| --------------- | -------------------------------------- | ------------------------------------- |
|
||||||
| `use` | `string` | (可选)引用已注册的全局 Action |
|
| `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 渲染 |
|
| `uiSchema` | `any` | (可选)用于 FlowSettings UI 渲染 |
|
||||||
|
| `paramsRequired`| `boolean` | (可选)是否需要参数配置,为 true 时添加模型前会打开配置对话框 |
|
||||||
|
| `hideInSettings`| `boolean` | (可选)是否在设置菜单中隐藏该步骤 |
|
||||||
| `handler` | `(ctx, params) => Promise<any>` | (可选)步骤执行逻辑,若未定义则使用 `use` 指定的全局 Action |
|
| `handler` | `(ctx, params) => Promise<any>` | (可选)步骤执行逻辑,若未定义则使用 `use` 指定的全局 Action |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -82,6 +82,116 @@
|
|||||||
- **flowSettings.openStepSettingsDialog(props: StepSettingsDialogProps)**
|
- **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
|
# 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 从远程加载模型数据。
|
根据唯一标识符 uid 从远程加载模型数据。
|
||||||
|
|
||||||
- **save(model: FlowModel): Promise<any>**
|
- **save(model: FlowModel): Promise<any>**
|
||||||
@ -19,7 +19,8 @@
|
|||||||
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||||
constructor(private app: Application) {}
|
constructor(private app: Application) {}
|
||||||
|
|
||||||
async load(uid: string) {
|
async findOne(query) {
|
||||||
|
const { uid, parentId } = query;
|
||||||
// 实现:根据 uid 获取模型
|
// 实现:根据 uid 获取模型
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,9 @@
|
|||||||
- **addSubModel(subKey: string, options): FlowModel**
|
- **addSubModel(subKey: string, options): FlowModel**
|
||||||
创建并添加一个子模型到数组字段(如 tabs、columns)。
|
创建并添加一个子模型到数组字段(如 tabs、columns)。
|
||||||
|
|
||||||
|
- **findSubModel\<K, R\>(subKey: K, callback: (model) => R): R**
|
||||||
|
查找子模型
|
||||||
|
|
||||||
- **mapSubModels\<K, R\>(subKey: K, callback: (model) => R): R[]**
|
- **mapSubModels\<K, R\>(subKey: K, callback: (model) => R): R[]**
|
||||||
遍历指定 key 的子模型,对每个子模型执行 callback 函数,并返回结果数组。
|
遍历指定 key 的子模型,对每个子模型执行 callback 函数,并返回结果数组。
|
||||||
- 支持完整的类型推导,callback 参数会自动推导为正确的模型类型
|
- 支持完整的类型推导,callback 参数会自动推导为正确的模型类型
|
||||||
@ -207,127 +210,3 @@ interface DefaultStructure {
|
|||||||
subModels?: Record<string, FlowModel | FlowModel[]>;
|
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。
|
- `setSourceId(sourceId) / getSourceId()`: 设置/获取源对象 ID。
|
||||||
- `setDataSourceKey(dataSourceKey) / getDataSourceKey()`: 设置/获取数据源标识(通过 header)。
|
- `setDataSourceKey(dataSourceKey) / getDataSourceKey()`: 设置/获取数据源标识(通过 header)。
|
||||||
- `setFilter(filter) / getFilter()`: 设置/获取过滤条件。
|
- `setFilter(filter) / getFilter()`: 设置/获取过滤条件。
|
||||||
|
- `addFilterGroup(key, filter) / removeFilterGroup(key)`: 设置/移除条件组。
|
||||||
- `setAppends(appends) / getAppends()`: 设置/获取附加字段。
|
- `setAppends(appends) / getAppends()`: 设置/获取附加字段。
|
||||||
- `addAppends(appends) / removeAppends(appends)`: 添加/移除附加字段。
|
- `addAppends(appends) / removeAppends(appends)`: 添加/移除附加字段。
|
||||||
- `setFilterByTk(filterByTk) / getFilterByTk()`: 设置/获取主键过滤条件。
|
- `setFilterByTk(filterByTk) / getFilterByTk()`: 设置/获取主键过滤条件。
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# FlowSubModel
|
# FlowSubModel
|
||||||
|
|
||||||
在 NocoBase 流引擎中,**子模型(SubModel)**是构建复杂模型树结构的核心能力。通过子模型机制,可以灵活地实现模型的嵌套、分组、组合等多层级结构,满足各种业务场景下的需求。
|
在 NocoBase 流引擎中,**子模型(SubModel)** 是构建复杂模型树结构的核心能力。通过子模型机制,可以灵活地实现模型的嵌套、分组、组合等多层级结构,满足各种业务场景下的需求。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -41,9 +41,16 @@ FlowModel 提供了丰富的 API 用于子模型的创建、添加、遍历和
|
|||||||
| `setParent(parent)` | 设置父模型 |
|
| `setParent(parent)` | 设置父模型 |
|
||||||
| `createRootModel(options)` | 创建根模型(通常由 flowEngine 调用) |
|
| `createRootModel(options)` | 创建根模型(通常由 flowEngine 调用) |
|
||||||
|
|
||||||
|
**注意事项**
|
||||||
|
|
||||||
|
- 推荐通过 `setSubModel` 和 `addSubModel` 方法管理子模型,避免直接操作 `subModels` 字段。
|
||||||
|
- 子模型字段不存在时会自动初始化为合适的类型(对象或数组)。
|
||||||
|
- 子模型的类型和结构建议通过泛型参数进行类型约束,提升类型安全和开发体验。
|
||||||
|
- 组件添加子模型时,通常会自动维护父子关系和数据同步。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 典型用法示例
|
## 用法示例
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// 创建根模型
|
// 创建根模型
|
||||||
@ -67,14 +74,14 @@ model.mapSubModels('tabs', (tab) => {
|
|||||||
## 子模型的父子关系
|
## 子模型的父子关系
|
||||||
|
|
||||||
- 每个子模型都自动维护对父模型的引用(`parent`)。
|
- 每个子模型都自动维护对父模型的引用(`parent`)。
|
||||||
- 父模型通过 `subModels` 字段管理所有子模型。
|
- 父模型通过 `subModels` 管理所有子模型。
|
||||||
- 通过 `setParent` 方法可手动设置父模型,但一般无需手动操作。
|
- 通过 `setParent` 方法可手动设置父模型,但一般无需手动操作。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 子模型的使用场景
|
## 子模型的使用场景
|
||||||
|
|
||||||
作为组件渲染
|
### 作为组件渲染
|
||||||
|
|
||||||
```tsx | pure
|
```tsx | pure
|
||||||
model.mapSubModels('tabs', (tab) => {
|
model.mapSubModels('tabs', (tab) => {
|
||||||
@ -82,7 +89,7 @@ model.mapSubModels('tabs', (tab) => {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
作为属性值使用
|
### 作为属性值使用
|
||||||
|
|
||||||
```tsx | pure
|
```tsx | pure
|
||||||
await model.applySubModelsAutoFlows(ctx);
|
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
|
||||||
|
|
||||||
- 用于向任意父模型添加任意类型的子模型。
|
<code src="./demos/flow-sub-model.tsx"></code>
|
||||||
- 支持自定义菜单项、回调、按钮内容等。
|
|
||||||
- 适用于绝大多数子模型添加场景。
|
|
||||||
|
|
||||||
**主要 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: {},
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 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 组织,是低代码建模和流程引擎的基础能力之一。
|
通过子模型机制与配套组件,NocoBase 支持灵活的模型树结构和动态 UI 组织,是低代码建模和流程引擎的基础能力之一。
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { Application, Plugin } from '@nocobase/client';
|
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 { Button, Dropdown, Input } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const dsm = new DataSourceManager();
|
const dsm = new DataSourceManager();
|
||||||
const ds = new DataSource({
|
const ds = new DataSource({
|
||||||
name: 'main',
|
key: 'main',
|
||||||
displayName: 'Main',
|
displayName: 'Main',
|
||||||
description: 'This is the main data source',
|
description: 'This is the main data source',
|
||||||
});
|
});
|
||||||
@ -47,7 +54,7 @@ ds.addCollection({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class FieldModel extends FlowModel {
|
class FieldModel extends FlowModel {
|
||||||
field: Field;
|
field: CollectionField;
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -94,7 +101,7 @@ class ConfigureFieldsFlowModel extends FlowModel<S> {
|
|||||||
getFieldMenuItems() {
|
getFieldMenuItems() {
|
||||||
return this.collection.mapFields((field) => {
|
return this.collection.mapFields((field) => {
|
||||||
return {
|
return {
|
||||||
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
|
key: `${this.collection.dataSource.key}.${this.collection.name}.${field.name}`,
|
||||||
label: field.title,
|
label: field.title,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -70,7 +70,7 @@ class ConfigureFieldsFlowModel extends FlowModel {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dsm.addDataSource({
|
dsm.addDataSource({
|
||||||
name: `ds-${uid()}`,
|
key: `ds-${uid()}`,
|
||||||
displayName: `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 React from 'react';
|
||||||
import { Application, Plugin } from '@nocobase/client';
|
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 MarkdownIt from 'markdown-it';
|
||||||
import Handlebars from 'handlebars';
|
import Handlebars from 'handlebars';
|
||||||
|
|
||||||
const Demo = () => {
|
const Demo = () => {
|
||||||
const uid = 'markdown-block';
|
const uid = 'markdown-block';
|
||||||
const model = useFlowModel<FlowModel>(uid, 'MarkdownModel');
|
const model = useFlowModelById<FlowModel>(uid, 'MarkdownModel');
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
|
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
|
||||||
<MarkdownBlock model={model} />
|
<MarkdownBlock model={model} />
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
|
|
||||||
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||||
constructor(private app: Application) {}
|
constructor(private app: Application) {}
|
||||||
async load(uid: string) {
|
async findOne({ uid, parentId }) {
|
||||||
// implement fetching a model by id
|
// implement fetching a model by id
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as icons from '@ant-design/icons';
|
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 { Application, Plugin } from '@nocobase/client';
|
||||||
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
|
||||||
import { Button, Modal } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
@ -83,6 +83,20 @@ const myEventFlow = defineFlow({
|
|||||||
},
|
},
|
||||||
title: '按钮事件',
|
title: '按钮事件',
|
||||||
steps: {
|
steps: {
|
||||||
|
modalWidth: {
|
||||||
|
title: '弹窗宽度配置',
|
||||||
|
uiSchema: {
|
||||||
|
width: {
|
||||||
|
type: 'string',
|
||||||
|
title: '弹窗宽度',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'NumberPicker',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler(ctx, params) {
|
||||||
|
return params.width || 520;
|
||||||
|
},
|
||||||
|
},
|
||||||
confirm: {
|
confirm: {
|
||||||
title: '确认操作配置',
|
title: '确认操作配置',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
@ -105,6 +119,7 @@ const myEventFlow = defineFlow({
|
|||||||
},
|
},
|
||||||
handler(ctx, params) {
|
handler(ctx, params) {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
|
width: ctx.stepResults.modalWidth,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -6,7 +6,9 @@ import { Button, Tabs } from 'antd';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React from 'react';
|
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() {
|
get models() {
|
||||||
const models = new Map();
|
const models = new Map();
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
@ -22,8 +24,12 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findOne(query) {
|
||||||
|
return this.load(query.uid);
|
||||||
|
}
|
||||||
|
|
||||||
// 从本地存储加载模型数据
|
// 从本地存储加载模型数据
|
||||||
async load(uid: string) {
|
async load({ uid }) {
|
||||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const json: FlowModel = JSON.parse(data);
|
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()));
|
localStorage.setItem(`flow-model:${subModel.uid}`, JSON.stringify(subModel.serialize()));
|
||||||
});
|
});
|
||||||
} else if (model.subModels[subModelKey] instanceof FlowModel) {
|
} 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;
|
return data;
|
||||||
@ -72,8 +81,7 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
|
|||||||
|
|
||||||
class TabFlowModel extends FlowModel {}
|
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) {
|
addTab(tab: any) {
|
||||||
// 使用新的 addSubModel API 添加子模型
|
// 使用新的 addSubModel API 添加子模型
|
||||||
const model = this.addSubModel('tabs', tab);
|
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) => ({
|
items={this.subModels.tabs?.map((tab) => ({
|
||||||
key: tab.getProps().key,
|
key: tab.getProps().key,
|
||||||
label: tab.getProps().label,
|
label: tab.getProps().label,
|
||||||
children: tab.render()
|
children: tab.render(),
|
||||||
}))}
|
}))}
|
||||||
tabBarExtraContent={
|
tabBarExtraContent={
|
||||||
<Button
|
<Button
|
||||||
@ -98,7 +106,7 @@ class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlo
|
|||||||
use: 'TabFlowModel',
|
use: 'TabFlowModel',
|
||||||
uid: tabId,
|
uid: tabId,
|
||||||
props: { key: tabId, label: `Tab - ${tabId}` },
|
props: { key: tabId, label: `Tab - ${tabId}` },
|
||||||
})
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Tab
|
Add Tab
|
||||||
@ -134,7 +142,7 @@ class PluginHelloModel extends Plugin {
|
|||||||
props: { key: 'tab-2', label: 'Tab 2' },
|
props: { key: 'tab-2', label: 'Tab 2' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
|
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FlowModel } from '@nocobase/flow-engine';
|
import { FlowModel } from '@nocobase/flow-engine';
|
||||||
import { Modal } from 'antd';
|
import { Button, Modal } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export class ActionModel extends FlowModel {
|
export class ActionModel extends FlowModel {
|
||||||
@ -8,7 +8,23 @@ export class ActionModel extends FlowModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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,
|
auto: true,
|
||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
|
uiSchema: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: '标题',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
'x-component-props': {
|
||||||
|
placeholder: '请输入标题',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
handler(ctx, params) {
|
handler(ctx, params) {
|
||||||
ctx.model.setProps('title', params.title);
|
ctx.model.setProps('title', params.title);
|
||||||
ctx.model.onClick = (e) => {
|
ctx.model.onClick = (e) => {
|
||||||
ctx.model.dispatchEvent('click', {
|
ctx.model.dispatchEvent('click', {
|
||||||
event: e,
|
event: e,
|
||||||
record: ctx.extra.record,
|
record: ctx.extra.record,
|
||||||
|
...ctx.extra,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -30,7 +58,7 @@ ActionModel.registerFlow({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
ActionModel.registerFlow({
|
LinkActionModel.registerFlow({
|
||||||
key: 'event1',
|
key: 'event1',
|
||||||
on: {
|
on: {
|
||||||
eventName: 'click',
|
eventName: 'click',
|
||||||
@ -38,7 +66,7 @@ ActionModel.registerFlow({
|
|||||||
steps: {
|
steps: {
|
||||||
step1: {
|
step1: {
|
||||||
handler(ctx, params) {
|
handler(ctx, params) {
|
||||||
Modal.confirm({
|
ctx.globals.modal.confirm({
|
||||||
title: `${ctx.extra.record?.id}`,
|
title: `${ctx.extra.record?.id}`,
|
||||||
content: 'Are you sure you want to perform this action?',
|
content: 'Are you sure you want to perform this action?',
|
||||||
onOk: async () => {},
|
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();
|
export const dsm = new DataSourceManager();
|
||||||
|
|
||||||
const ds = new DataSource({
|
const ds = new DataSource({
|
||||||
name: 'main',
|
key: 'main',
|
||||||
displayName: 'Main',
|
displayName: 'Main',
|
||||||
description: 'This is the main data source',
|
description: 'This is the main data source',
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { FlowModelRenderer } from '@nocobase/flow-engine';
|
import { FlowModelRenderer } from '@nocobase/flow-engine';
|
||||||
|
import actions from 'packages/plugins/@nocobase/plugin-workflow/src/server/actions';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createApp } from '../createApp';
|
import { createApp } from '../createApp';
|
||||||
import { FormItemModel } from '../form/form-item-model';
|
import { FormItemModel } from '../form/form-item-model';
|
||||||
import { FormModel } from '../form/form-model';
|
import { FormModel } from '../form/form-model';
|
||||||
import { SubmitActionModel } from '../form/submit-action-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 { dsm } from './data-source-manager';
|
||||||
import { TableColumnActionsModel, TableColumnModel } from './table-column-model';
|
import { TableColumnActionsModel, TableColumnModel } from './table-column-model';
|
||||||
import { TableModel } from './table-model';
|
import { TableModel } from './table-model';
|
||||||
@ -14,6 +15,8 @@ class PluginDemo extends Plugin {
|
|||||||
async load() {
|
async load() {
|
||||||
this.flowEngine.context.dsm = dsm;
|
this.flowEngine.context.dsm = dsm;
|
||||||
this.flowEngine.registerModels({
|
this.flowEngine.registerModels({
|
||||||
|
DeleteActionModel,
|
||||||
|
LinkActionModel,
|
||||||
FormModel,
|
FormModel,
|
||||||
FormItemModel,
|
FormItemModel,
|
||||||
SubmitActionModel,
|
SubmitActionModel,
|
||||||
@ -33,13 +36,25 @@ class PluginDemo extends Plugin {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
subModels: {
|
subModels: {
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
use: 'DeleteActionModel',
|
||||||
|
stepParams: {
|
||||||
|
default: {
|
||||||
|
step1: {
|
||||||
|
title: 'Delete',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
use: 'TableColumnActionsModel',
|
use: 'TableColumnActionsModel',
|
||||||
subModels: {
|
subModels: {
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
use: 'ActionModel',
|
use: 'LinkActionModel',
|
||||||
stepParams: {
|
stepParams: {
|
||||||
default: {
|
default: {
|
||||||
step1: {
|
step1: {
|
||||||
@ -49,7 +64,7 @@ class PluginDemo extends Plugin {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
use: 'ActionModel',
|
use: 'LinkActionModel',
|
||||||
stepParams: {
|
stepParams: {
|
||||||
default: {
|
default: {
|
||||||
step1: {
|
step1: {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
import { css } from '@emotion/css';
|
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 { Space } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormModel } from '../form/form-model';
|
import { FormModel } from '../form/form-model';
|
||||||
import { ActionModel } from './action-model';
|
import { ActionModel } from './action-model';
|
||||||
|
|
||||||
export class TableColumnModel extends FlowModel {
|
export class TableColumnModel extends FlowModel {
|
||||||
field: Field;
|
field: CollectionField;
|
||||||
fieldPath: string;
|
fieldPath: string;
|
||||||
|
|
||||||
getColumnProps() {
|
getColumnProps() {
|
||||||
@ -75,7 +75,12 @@ export class TableColumnActionsModel extends TableColumnModel {
|
|||||||
return (value, record, index) => (
|
return (value, record, index) => (
|
||||||
<Space>
|
<Space>
|
||||||
{this.mapSubModels('actions', (action: ActionModel) => (
|
{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>
|
</Space>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import { Collection, FlowModel, MultiRecordResource } from '@nocobase/flow-engine';
|
import { SettingOutlined } from '@ant-design/icons';
|
||||||
import { Button, Dropdown, Table } from 'antd';
|
import { AddActionModel, Collection, FlowModel, FlowModelRenderer, MultiRecordResource } from '@nocobase/flow-engine';
|
||||||
|
import { Button, Dropdown, Space, Table } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ActionModel } from './action-model';
|
||||||
import { api } from './api';
|
import { api } from './api';
|
||||||
import { TableColumnModel } from './table-column-model';
|
import { TableColumnModel } from './table-column-model';
|
||||||
|
|
||||||
type S = {
|
type S = {
|
||||||
subModels: {
|
subModels: {
|
||||||
columns: TableColumnModel[];
|
columns: TableColumnModel[];
|
||||||
|
actions: ActionModel[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,7 +39,7 @@ export class TableModel extends FlowModel<S> {
|
|||||||
},
|
},
|
||||||
items: this.collection.mapFields((field) => {
|
items: this.collection.mapFields((field) => {
|
||||||
return {
|
return {
|
||||||
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
|
key: `${this.collection.dataSource.key}.${this.collection.name}.${field.name}`,
|
||||||
label: field.title,
|
label: field.title,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@ -51,10 +54,43 @@ export class TableModel extends FlowModel<S> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={this.resource.getData()}
|
dataSource={this.resource.getData()}
|
||||||
columns={this.getColumns()}
|
columns={this.getColumns()}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
onChange: (_, selectedRows) => {
|
||||||
|
this.resource.setSelectedRows(selectedRows);
|
||||||
|
},
|
||||||
|
selectedRowKeys: this.resource.getSelectedRows().map((row) => row.id),
|
||||||
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: this.resource.getMeta('page'),
|
current: this.resource.getMeta('page'),
|
||||||
pageSize: this.resource.getMeta('pageSize'),
|
pageSize: this.resource.getMeta('pageSize'),
|
||||||
|
@ -20,6 +20,10 @@
|
|||||||
|
|
||||||
<code src="./demos/register-flow.tsx"></code>
|
<code src="./demos/register-flow.tsx"></code>
|
||||||
|
|
||||||
|
## 动态默认配置参数
|
||||||
|
|
||||||
|
<code src="./demos/dynamic-default-params.tsx"></code>
|
||||||
|
|
||||||
## table block
|
## table block
|
||||||
|
|
||||||
<code src="./demos/table-block.tsx"></code>
|
<code src="./demos/table-block.tsx"></code>
|
||||||
@ -84,3 +88,6 @@
|
|||||||
|
|
||||||
<code src="./demos/open-required-step-params-dialog.tsx"></code>
|
<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",
|
"name": "@nocobase/client",
|
||||||
"version": "1.8.0-alpha.5",
|
"version": "1.8.0-alpha.9",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"module": "es/index.mjs",
|
"module": "es/index.mjs",
|
||||||
@ -26,9 +26,9 @@
|
|||||||
"@formily/reactive-react": "^2.2.27",
|
"@formily/reactive-react": "^2.2.27",
|
||||||
"@formily/shared": "^2.2.27",
|
"@formily/shared": "^2.2.27",
|
||||||
"@formily/validator": "^2.2.27",
|
"@formily/validator": "^2.2.27",
|
||||||
"@nocobase/evaluators": "1.8.0-alpha.5",
|
"@nocobase/evaluators": "1.8.0-alpha.9",
|
||||||
"@nocobase/sdk": "1.8.0-alpha.5",
|
"@nocobase/sdk": "1.8.0-alpha.9",
|
||||||
"@nocobase/utils": "1.8.0-alpha.5",
|
"@nocobase/utils": "1.8.0-alpha.9",
|
||||||
"ahooks": "^3.7.2",
|
"ahooks": "^3.7.2",
|
||||||
"antd": "5.24.2",
|
"antd": "5.24.2",
|
||||||
"antd-style": "3.7.1",
|
"antd-style": "3.7.1",
|
||||||
@ -65,6 +65,8 @@
|
|||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
"react-to-print": "^2.14.7",
|
"react-to-print": "^2.14.7",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
|
"tabulator-tables": "^6.3.1",
|
||||||
|
"@types/tabulator-tables": "^6.2.6",
|
||||||
"use-deep-compare-effect": "^1.8.1"
|
"use-deep-compare-effect": "^1.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -72,7 +72,7 @@ export class APIClient extends APIClientSDK {
|
|||||||
api.notification = this.notification;
|
api.notification = this.notification;
|
||||||
const handlers = [];
|
const handlers = [];
|
||||||
for (const handler of this.axios.interceptors.response['handlers']) {
|
for (const handler of this.axios.interceptors.response['handlers']) {
|
||||||
if (handler.rejected['_name'] === 'handleNotificationError') {
|
if (handler?.rejected?.['_name'] === 'handleNotificationError') {
|
||||||
handlers.push({
|
handlers.push({
|
||||||
...handler,
|
...handler,
|
||||||
rejected: api.handleNotificationError.bind(api),
|
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 { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
|
||||||
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
|
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 type { CollectionFieldInterfaceFactory } from '../data-source';
|
||||||
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
||||||
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||||
@ -273,8 +273,14 @@ export class Application {
|
|||||||
this.use(AntdAppProvider);
|
this.use(AntdAppProvider);
|
||||||
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
|
||||||
this.use(OpenModeProvider);
|
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(FlowEngineProvider, { engine: this.flowEngine });
|
||||||
|
this.use(FlowEngineGlobalsContextProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addReactRouterComponents() {
|
private addReactRouterComponents() {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { Divider, Empty, Input, MenuProps } from 'antd';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { useCompile } from '../../../';
|
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 { FormItemSchemaToolbar } from '../modules/blocks/data-blocks/form/FormItemSchemaToolbar';
|
||||||
import { useCreateFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps';
|
import { useCreateFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockDecoratorProps';
|
||||||
import { useCreateFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useCreateFormBlockProps';
|
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 { useEditFormBlockDecoratorProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockDecoratorProps';
|
||||||
import { useEditFormBlockProps } from '../modules/blocks/data-blocks/form/hooks/useEditFormBlockProps';
|
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 {
|
import {
|
||||||
useGridCardBlockDecoratorProps,
|
useGridCardBlockDecoratorProps,
|
||||||
useGridCardBlockItemProps,
|
useGridCardBlockItemProps,
|
||||||
@ -97,6 +98,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
|||||||
useGridCardBlockProps,
|
useGridCardBlockProps,
|
||||||
useFormItemProps,
|
useFormItemProps,
|
||||||
useDataFormItemProps,
|
useDataFormItemProps,
|
||||||
|
useGridCardActionBarProps,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
@ -161,6 +163,7 @@ export class BlockSchemaComponentPlugin extends Plugin {
|
|||||||
useGridCardBlockItemProps,
|
useGridCardBlockItemProps,
|
||||||
useFormItemProps,
|
useFormItemProps,
|
||||||
useDataFormItemProps,
|
useDataFormItemProps,
|
||||||
|
useGridCardActionBarProps,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ interface Props {
|
|||||||
expandFlag?: boolean;
|
expandFlag?: boolean;
|
||||||
dragSortBy?: string;
|
dragSortBy?: string;
|
||||||
association?: string;
|
association?: string;
|
||||||
enableIndexÏColumn?: boolean;
|
enableIndexColumn?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InternalTableBlockProvider = (props: Props) => {
|
const InternalTableBlockProvider = (props: Props) => {
|
||||||
@ -77,7 +77,7 @@ const InternalTableBlockProvider = (props: Props) => {
|
|||||||
fieldNames,
|
fieldNames,
|
||||||
collection,
|
collection,
|
||||||
association,
|
association,
|
||||||
enableIndexÏColumn,
|
enableIndexColumn,
|
||||||
} = props;
|
} = props;
|
||||||
const field: any = useField();
|
const field: any = useField();
|
||||||
const { resource, service } = useBlockRequestContext();
|
const { resource, service } = useBlockRequestContext();
|
||||||
@ -136,7 +136,7 @@ const InternalTableBlockProvider = (props: Props) => {
|
|||||||
setExpandFlag: setExpandFlagValue,
|
setExpandFlag: setExpandFlagValue,
|
||||||
heightProps,
|
heightProps,
|
||||||
association,
|
association,
|
||||||
enableIndexÏColumn,
|
enableIndexColumn,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
allIncludesChildren,
|
allIncludesChildren,
|
||||||
@ -153,7 +153,7 @@ const InternalTableBlockProvider = (props: Props) => {
|
|||||||
setExpandFlagValue,
|
setExpandFlagValue,
|
||||||
showIndex,
|
showIndex,
|
||||||
association,
|
association,
|
||||||
enableIndexÏColumn,
|
enableIndexColumn,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -197,9 +197,7 @@ export function useCollectValuesToSubmit() {
|
|||||||
|
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
||||||
if (parsedValue !== null && parsedValue !== undefined) {
|
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
|
||||||
}
|
|
||||||
} else if (value !== '') {
|
} else if (value !== '') {
|
||||||
assignedValues[key] = value;
|
assignedValues[key] = value;
|
||||||
}
|
}
|
||||||
@ -385,9 +383,7 @@ export const useAssociationCreateActionProps = () => {
|
|||||||
|
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
||||||
if (parsedValue) {
|
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
|
||||||
}
|
|
||||||
} else if (value !== '') {
|
} else if (value !== '') {
|
||||||
assignedValues[key] = value;
|
assignedValues[key] = value;
|
||||||
}
|
}
|
||||||
@ -658,9 +654,7 @@ export const useCustomizeUpdateActionProps = () => {
|
|||||||
|
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
||||||
if (parsedValue) {
|
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
|
||||||
}
|
|
||||||
} else if (value !== '') {
|
} else if (value !== '') {
|
||||||
assignedValues[key] = value;
|
assignedValues[key] = value;
|
||||||
}
|
}
|
||||||
@ -771,9 +765,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
|||||||
|
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
||||||
if (parsedValue) {
|
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
|
||||||
}
|
|
||||||
} else if (value !== '') {
|
} else if (value !== '') {
|
||||||
assignedValues[key] = value;
|
assignedValues[key] = value;
|
||||||
}
|
}
|
||||||
@ -999,9 +991,7 @@ export const useUpdateActionProps = () => {
|
|||||||
|
|
||||||
if (isVariable(value)) {
|
if (isVariable(value)) {
|
||||||
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
const { value: parsedValue } = (await variables?.parseVariable(value, localVariables)) || {};
|
||||||
if (parsedValue) {
|
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
||||||
assignedValues[key] = transformVariableValue(parsedValue, { targetCollectionField: collectionField });
|
|
||||||
}
|
|
||||||
} else if (value !== '') {
|
} else if (value !== '') {
|
||||||
assignedValues[key] = value;
|
assignedValues[key] = value;
|
||||||
}
|
}
|
||||||
|
@ -193,6 +193,8 @@ export const EditFieldAction = (props) => {
|
|||||||
defaultValues.reverseField = interfaceConf?.default?.reverseField;
|
defaultValues.reverseField = interfaceConf?.default?.reverseField;
|
||||||
set(defaultValues.reverseField, 'name', `f_${uid()}`);
|
set(defaultValues.reverseField, 'name', `f_${uid()}`);
|
||||||
set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title);
|
set(defaultValues.reverseField, 'uiSchema.title', record.__parent?.title);
|
||||||
|
} else {
|
||||||
|
defaultValues.autoCreateReverseField = true;
|
||||||
}
|
}
|
||||||
const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer);
|
const schema = getSchema(interfaceConf, defaultValues, record, compile, getContainer);
|
||||||
setSchema(schema);
|
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 form = useForm();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
@ -410,9 +410,14 @@ export const useCreateAction = (actionCallback?: (values: any) => void) => {
|
|||||||
await form.submit();
|
await form.submit();
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.loading = true;
|
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 });
|
const res = await resource.create({ values: form.values });
|
||||||
ctx.setVisible(false);
|
ctx.setVisible(false);
|
||||||
actionCallback?.(res?.data?.data);
|
await actionCallback?.(res?.data?.data, collections);
|
||||||
await form.reset();
|
await form.reset();
|
||||||
field.data.loading = false;
|
field.data.loading = false;
|
||||||
refresh();
|
refresh();
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
import { Plugin } from '../application/Plugin';
|
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 {
|
import {
|
||||||
CheckboxFieldInterface,
|
CheckboxFieldInterface,
|
||||||
CheckboxGroupFieldInterface,
|
CheckboxGroupFieldInterface,
|
||||||
@ -53,14 +54,13 @@ import {
|
|||||||
UrlFieldInterface,
|
UrlFieldInterface,
|
||||||
UUIDFieldInterface,
|
UUIDFieldInterface,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
import { InheritanceCollectionMixin } from './mixins/InheritanceCollectionMixin';
|
||||||
import {
|
import {
|
||||||
GeneralCollectionTemplate,
|
GeneralCollectionTemplate,
|
||||||
SqlCollectionTemplate,
|
SqlCollectionTemplate,
|
||||||
TreeCollectionTemplate,
|
TreeCollectionTemplate,
|
||||||
ViewCollectionTemplate,
|
ViewCollectionTemplate,
|
||||||
} from './templates';
|
} 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 {
|
class MainDataSource extends DataSource {
|
||||||
async getDataSource() {
|
async getDataSource() {
|
||||||
@ -72,6 +72,7 @@ class MainDataSource extends DataSource {
|
|||||||
const collections = service?.data?.data || [];
|
const collections = service?.data?.data || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
key: 'main',
|
||||||
collections,
|
collections,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ export class CheckboxFieldInterface extends CollectionFieldInterface {
|
|||||||
'x-component': 'Checkbox',
|
'x-component': 'Checkbox',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
availableTypes = ['boolean', 'integer', 'bigInt'];
|
availableTypes = ['boolean', 'integer', 'bigInt', 'bit'];
|
||||||
hasDefaultValue = true;
|
hasDefaultValue = true;
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...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 = [
|
export const tableoid = [
|
||||||
|
@ -159,7 +159,7 @@ export class InheritanceCollectionMixin extends Collection {
|
|||||||
const targetField = filterFields.find((k) => {
|
const targetField = filterFields.find((k) => {
|
||||||
return k.name === v.name;
|
return k.name === v.name;
|
||||||
});
|
});
|
||||||
return targetField.collectionName !== this.name;
|
return targetField?.collectionName !== this.name;
|
||||||
});
|
});
|
||||||
return this.parentCollectionFields[parentCollectionName];
|
return this.parentCollectionFields[parentCollectionName];
|
||||||
}
|
}
|
||||||
|
@ -58,9 +58,13 @@ export const fieldComponentSettingsItem: SchemaSettingsItemType = {
|
|||||||
value: fieldSchema['x-component-props']?.['component'] || options[0]?.value,
|
value: fieldSchema['x-component-props']?.['component'] || options[0]?.value,
|
||||||
onChange(component) {
|
onChange(component) {
|
||||||
const componentOptions = options.find((item) => item.value === component);
|
const componentOptions = options.find((item) => item.value === component);
|
||||||
|
const baseProps = componentOptions?.useProps?.() || {};
|
||||||
const componentProps = {
|
const componentProps = {
|
||||||
component,
|
component,
|
||||||
...(componentOptions?.useProps?.() || {}),
|
...baseProps,
|
||||||
|
...(component === collectionField['uiSchema']['x-component']
|
||||||
|
? collectionField['uiSchema']['x-component-props']
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
_.set(fieldSchema, 'x-component-props', componentProps);
|
_.set(fieldSchema, 'x-component-props', componentProps);
|
||||||
field.componentProps = componentProps;
|
field.componentProps = componentProps;
|
||||||
|
@ -83,8 +83,18 @@ export abstract class DataSource {
|
|||||||
|
|
||||||
abstract getDataSource(): Promise<Omit<Partial<DataSourceOptions>, 'key'>> | Omit<Partial<DataSourceOptions>, 'key'>;
|
abstract getDataSource(): Promise<Omit<Partial<DataSourceOptions>, 'key'>> | Omit<Partial<DataSourceOptions>, 'key'>;
|
||||||
|
|
||||||
|
get flowEngineDataSourceManager() {
|
||||||
|
return this.app.flowEngine?.context?.dataSourceManager;
|
||||||
|
}
|
||||||
|
|
||||||
async reload() {
|
async reload() {
|
||||||
const dataSource = await this.getDataSource();
|
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.setOptions(dataSource);
|
||||||
this.collectionManager.setCollections(dataSource.collections || []);
|
this.collectionManager.setCollections(dataSource.collections || []);
|
||||||
this.reloadCallbacks.forEach((callback) => callback(dataSource.collections));
|
this.reloadCallbacks.forEach((callback) => callback(dataSource.collections));
|
||||||
|
@ -118,11 +118,17 @@ export const transformToFilter = (
|
|||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (value?.type) {
|
|
||||||
|
const collectionField = getCollectionJoinField(`${collectionName}.${path}`);
|
||||||
|
|
||||||
|
if (
|
||||||
|
['datetime', 'datetimeNoTz', 'date', 'unixTimestamp', 'createdAt', 'updatedAt'].includes(
|
||||||
|
collectionField?.interface,
|
||||||
|
)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionField = getCollectionJoinField(`${collectionName}.${path}`);
|
|
||||||
if (collectionField?.target) {
|
if (collectionField?.target) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { FlowModel, IFlowModelRepository } from '@nocobase/flow-engine';
|
import { FlowModel, IFlowModelRepository } from '@nocobase/flow-engine';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { Application } from '../application';
|
||||||
|
|
||||||
export class MockFlowModelRepository implements IFlowModelRepository<FlowModel> {
|
export class MockFlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||||
get models() {
|
get models() {
|
||||||
@ -26,6 +27,26 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
|||||||
return models;
|
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) {
|
async load(uid: string) {
|
||||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||||
@ -43,7 +64,9 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
|||||||
json.subModels[model.subKey].push(subModel);
|
json.subModels[model.subKey].push(subModel);
|
||||||
} else if (model.subType === 'object') {
|
} else if (model.subType === 'object') {
|
||||||
const subModel = await this.load(model.uid);
|
const subModel = await this.load(model.uid);
|
||||||
json.subModels[model.subKey] = subModel;
|
if (subModel) {
|
||||||
|
json.subModels[model.subKey] = subModel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,3 +100,32 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
|||||||
return true;
|
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.
|
* 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 { useRequest } from 'ahooks';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
function InternalFlowPage({ uid }) {
|
function InternalFlowPage({ uid, sharedContext }) {
|
||||||
const model = useFlowModel(uid);
|
const model = useFlowModelById(uid);
|
||||||
return <FlowModelRenderer model={model} showFlowSettings hideRemoveInSettings />;
|
return (
|
||||||
|
<FlowModelRenderer
|
||||||
|
model={model}
|
||||||
|
sharedContext={sharedContext}
|
||||||
|
showFlowSettings={{ showBackground: false, showBorder: false }}
|
||||||
|
hideRemoveInSettings
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlowPage = () => {
|
export const FlowRoute = () => {
|
||||||
const params = useParams();
|
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 flowEngine = useFlowEngine();
|
||||||
const { loading } = useRequest(
|
const { loading, data } = useRequest(
|
||||||
() => {
|
async () => {
|
||||||
return flowEngine.loadOrCreateModel({
|
const options = {
|
||||||
uid: uid,
|
uid,
|
||||||
use: 'PageFlowModel',
|
use: 'PageModel',
|
||||||
subModels: {
|
subModels: {
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
use: 'PageTabFlowModel',
|
use: 'PageTabModel',
|
||||||
subModels: {
|
subModels: {
|
||||||
grid: {
|
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 <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 { DataSource, DataSourceManager, FlowModel } from '@nocobase/flow-engine';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Plugin } from '../application/Plugin';
|
import { Plugin } from '../application/Plugin';
|
||||||
|
import * as actions from './actions';
|
||||||
import { FlowEngineRunner } from './FlowEngineRunner';
|
import { FlowEngineRunner } from './FlowEngineRunner';
|
||||||
import { MockFlowModelRepository } from './FlowModelRepository';
|
import { FlowModelRepository, MockFlowModelRepository } from './FlowModelRepository';
|
||||||
import { FlowPage } from './FlowPage';
|
import { FlowRoute } from './FlowPage';
|
||||||
|
import { DateTimeFormat } from './flowSetting/DateTimeFormat';
|
||||||
import * as models from './models';
|
import * as models from './models';
|
||||||
|
|
||||||
export class PluginFlowEngine extends Plugin {
|
export class PluginFlowEngine extends Plugin {
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addComponents({ FlowPage });
|
this.app.addComponents({ FlowRoute });
|
||||||
this.app.flowEngine.setModelRepository(new MockFlowModelRepository());
|
// this.app.flowEngine.setModelRepository(new MockFlowModelRepository());
|
||||||
|
this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
|
||||||
const filteredModels = Object.fromEntries(
|
const filteredModels = Object.fromEntries(
|
||||||
Object.entries(models).filter(
|
Object.entries(models).filter(
|
||||||
([, ModelClass]) => typeof ModelClass === 'function' && ModelClass.prototype instanceof FlowModel,
|
([, ModelClass]) => typeof ModelClass === 'function' && ModelClass.prototype instanceof FlowModel,
|
||||||
),
|
),
|
||||||
);
|
) as Record<string, typeof FlowModel>;
|
||||||
console.log('Registering flow models:', Object.keys(filteredModels));
|
// console.log('Registering flow models:', Object.keys(filteredModels));
|
||||||
this.flowEngine.registerModels(filteredModels);
|
this.flowEngine.registerModels(filteredModels);
|
||||||
|
this.flowEngine.registerActions(actions);
|
||||||
const dataSourceManager = new DataSourceManager();
|
const dataSourceManager = new DataSourceManager();
|
||||||
this.flowEngine.context['app'] = this.app;
|
this.flowEngine.context['flowEngine'] = this.flowEngine;
|
||||||
this.flowEngine.context['api'] = this.app.apiClient;
|
|
||||||
this.flowEngine.context['dataSourceManager'] = dataSourceManager;
|
this.flowEngine.context['dataSourceManager'] = dataSourceManager;
|
||||||
try {
|
const mainDataSource = new DataSource({
|
||||||
const response = await this.app.apiClient.request<any>({
|
key: 'main',
|
||||||
url: '/collections:listMeta',
|
displayName: 'Main',
|
||||||
});
|
});
|
||||||
const mainDataSource = new DataSource({
|
dataSourceManager.addDataSource(mainDataSource);
|
||||||
name: '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, {});
|
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