mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
Merge branch '2.0' into feat/actions-2.0
This commit is contained in:
commit
0306ba6423
@ -0,0 +1,87 @@
|
||||
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() {
|
||||
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);
|
||||
|
||||
class PluginDemo extends Plugin {
|
||||
async load() {
|
||||
this.flowEngine.registerModels({ MyModel });
|
||||
this.flowEngine.registerAction({
|
||||
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();
|
||||
}
|
||||
},
|
||||
});
|
||||
const model = this.flowEngine.createModel({
|
||||
use: 'MyModel',
|
||||
});
|
||||
this.router.add('root', {
|
||||
path: '/',
|
||||
element: <FlowModelRenderer model={model} showFlowSettings />,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default createApp({ plugins: [PluginDemo] });
|
@ -7,3 +7,7 @@
|
||||
## 弹窗
|
||||
|
||||
<code src="./demos/popup.tsx"></code>
|
||||
|
||||
## Confirm
|
||||
|
||||
<code src="./demos/confirm.tsx"></code>
|
||||
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||
// 实现一个本地存储的模型仓库,负责模型的持久化
|
||||
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||
// 从本地存储加载模型数据
|
||||
async load(uid: string) {
|
||||
async findOne({ uid }) {
|
||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||
if (!data) return null;
|
||||
return JSON.parse(data);
|
||||
|
@ -1,6 +1,6 @@
|
||||
# FlowAction
|
||||
|
||||
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作(Action)的核心对象。每个操作(Action)封装一段可执行的业务逻辑,可以在多个流步骤中复用,支持参数配置、UI 配置和类型推断。
|
||||
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作(Action)的核心对象。每个操作封装一段可执行的业务逻辑,可在多个流步骤中复用,并支持参数配置、UI 配置和类型推断。
|
||||
|
||||
---
|
||||
|
||||
@ -10,48 +10,50 @@
|
||||
interface ActionDefinition {
|
||||
name: string; // 操作唯一标识,必须唯一
|
||||
title?: string; // 操作显示名称(可选)
|
||||
uiSchema?: Record<string, ISchema>; // (可选)用于参数配置界面渲染
|
||||
uiSchema?: Record<string, ISchema>; // (可选)参数配置界面渲染
|
||||
defaultParams?: Record<string, any>; // (可选)默认参数
|
||||
paramsRequired?: boolean; // (可选)是否需要参数配置,为true时添加模型前会打开配置对话框
|
||||
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏该步骤
|
||||
paramsRequired?: boolean; // (可选)是否需要参数配置
|
||||
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏
|
||||
handler: (ctx: FlowContext, params: any) => Promise<any> | any; // 操作执行逻辑
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 定义操作的方式
|
||||
## 使用说明
|
||||
|
||||
### 1. 使用 defineAction 工具函数
|
||||
### 1. 定义 Action
|
||||
|
||||
推荐方式,结构清晰、类型推断友好:
|
||||
#### 方式一:使用 defineAction 工具函数(推荐)
|
||||
|
||||
结构清晰,类型推断友好:
|
||||
|
||||
```ts
|
||||
const myAction = defineAction({
|
||||
name: 'actionName',
|
||||
name: 'myAction',
|
||||
title: '操作显示名称',
|
||||
uiSchema: {},
|
||||
defaultParams: {},
|
||||
paramsRequired: true, // 添加模型前强制打开配置对话框
|
||||
hideInSettings: false, // 在设置菜单中显示
|
||||
paramsRequired: true,
|
||||
hideInSettings: false,
|
||||
async handler(ctx, params) {
|
||||
// 操作逻辑
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 实现 ActionDefinition 接口
|
||||
#### 方式二:实现 ActionDefinition 接口
|
||||
|
||||
适合需要扩展属性或方法的场景:
|
||||
复杂场景时可以通过定义 Action 类来处理更复杂的操作
|
||||
|
||||
```ts
|
||||
class MyAction implements ActionDefinition {
|
||||
name = 'actionName';
|
||||
name = 'myAction';
|
||||
title = '操作显示名称';
|
||||
uiSchema = {};
|
||||
defaultParams = {};
|
||||
paramsRequired = true; // 添加模型前强制打开配置对话框
|
||||
hideInSettings = false; // 在设置菜单中显示
|
||||
paramsRequired = true;
|
||||
hideInSettings = false;
|
||||
async handler(ctx, params) {
|
||||
// 操作逻辑
|
||||
}
|
||||
@ -60,59 +62,115 @@ class MyAction implements ActionDefinition {
|
||||
|
||||
---
|
||||
|
||||
## 注册操作
|
||||
|
||||
注册后可在流步骤中通过 `use` 字段复用:
|
||||
### 2. 注册到 FlowEngine 里
|
||||
|
||||
```ts
|
||||
flowEngine.registerAction({
|
||||
name: 'actionName',
|
||||
title: '操作显示名称',
|
||||
uiSchema: {},
|
||||
defaultParams: {},
|
||||
paramsRequired: true, // 添加模型前强制打开配置对话框
|
||||
hideInSettings: false, // 在设置菜单中显示
|
||||
handler(ctx, params) {
|
||||
// 操作逻辑
|
||||
},
|
||||
});
|
||||
|
||||
flowEngine.registerAction(myAction); // 注册 defineAction 返回的对象
|
||||
flowEngine.registerAction(new MyAction()); // 注册类实例
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在流中复用操作
|
||||
### 3. 在流中使用
|
||||
|
||||
在流步骤定义中通过 `use` 字段引用已注册的操作:
|
||||
|
||||
```ts
|
||||
steps: {
|
||||
step1: {
|
||||
use: 'actionName', // 复用已注册的操作
|
||||
use: 'myAction', // 复用已注册的操作
|
||||
defaultParams: {},
|
||||
paramsRequired: true, // 可以在步骤级别覆盖操作的paramsRequired设置
|
||||
hideInSettings: false, // 可以在步骤级别覆盖操作的hideInSettings设置
|
||||
paramsRequired: true, // 可覆盖操作的 paramsRequired
|
||||
hideInSettings: false, // 可覆盖操作的 hideInSettings
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置选项说明
|
||||
## 参数配置详解
|
||||
|
||||
### name
|
||||
|
||||
- **类型**: `string`
|
||||
- **说明**: 操作唯一标识,必须全局唯一。建议使用有业务含义的英文名,便于维护和复用。
|
||||
|
||||
### title
|
||||
|
||||
- **类型**: `string`
|
||||
- **说明**: 操作的显示名称,通常用于界面展示。支持多语言配置。
|
||||
|
||||
### defaultParams
|
||||
|
||||
- **类型**: `Record<string, any>` 或 `(ctx) => Record<string, any>`
|
||||
- **说明**: 操作参数的默认值。支持静态对象或函数(可根据 context 动态生成)。
|
||||
- **作用**: 作为 handler 的 params 默认值。
|
||||
|
||||
**静态用法:**
|
||||
```ts
|
||||
{
|
||||
defaultParams: { key1: 'val1' },
|
||||
async handler(ctx, params) {
|
||||
console.log(params.key1); // val1
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**动态用法:**
|
||||
```ts
|
||||
{
|
||||
defaultParams(ctx) {
|
||||
return { key1: 'val1' }
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
console.log(params.key1); // val1
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### handler
|
||||
|
||||
- **类型**: `(ctx: FlowContext, params: any) => Promise<any> | any`
|
||||
- **说明**: 操作的核心执行逻辑。支持异步和同步函数。`ctx` 提供当前流上下文,`params` 为参数对象。
|
||||
|
||||
### uiSchema
|
||||
|
||||
- **类型**: `Omit<FormilySchema, 'default'>`
|
||||
- **说明**: 用于参数的可视化配置表单。推荐与 defaultParams 配合使用,提升用户体验。
|
||||
- **注意**: uiSchema 不支持 default 参数,避免与 defaultParams 重复。
|
||||
|
||||
### paramsRequired
|
||||
|
||||
- **类型**: `boolean`
|
||||
- **默认值**: `false`
|
||||
- **说明**: 当设置为 `true` 时,在添加该步骤模型前会强制打开参数配置对话框,确保用户配置必要的参数。适用于需要用户必须配置参数才能正常工作的操作。
|
||||
- **说明**: 为 `true` 时,添加步骤前会强制打开参数配置对话框,确保用户配置必要参数。适用于参数必填的场景。
|
||||
|
||||
### hideInSettings
|
||||
|
||||
- **类型**: `boolean`
|
||||
- **默认值**: `false`
|
||||
- **说明**: 当设置为 `true` 时,该步骤将在设置菜单中隐藏,用户无法通过 Settings 界面直接添加该步骤。适用于初始化配置场景。
|
||||
- **说明**: 为 `true` 时,该步骤在设置菜单中隐藏,用户无法通过 Settings 界面直接添加。适用于初始化配置或内部步骤。
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
- 推荐优先使用 `defineAction` 工具函数定义操作,结构更清晰,类型推断更友好。
|
||||
- `name` 字段建议采用有业务含义的英文名,避免重名。
|
||||
- `uiSchema` 和 `defaultParams` 配合使用,提升参数配置体验。
|
||||
- 对于需要用户强制配置参数的操作,设置 `paramsRequired: true`。
|
||||
- 内部或自动化步骤可设置 `hideInSettings: true`,避免用户误操作。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题与注意事项
|
||||
|
||||
- **Q: defaultParams 和 uiSchema 有什么区别?**
|
||||
> defaultParams 用于设置参数默认值,uiSchema 用于渲染参数配置表单。两者配合使用,互不冲突。
|
||||
- **Q: uiSchema 为什么不支持 default?**
|
||||
> 1. 为避免与 defaultParams 重复,uiSchema 仅用于表单结构描述,不处理默认值;
|
||||
> 2. 使用 defaultParams 处理可以有更好的 ts 类型提示;
|
||||
> 3. uiSchema 的结构可能较为复杂,解析 uiSchema 来提取 default 值非常繁琐且容易出错,因此不建议在 uiSchema 中处理 default;
|
||||
|
||||
---
|
||||
|
||||
@ -121,3 +179,34 @@ steps: {
|
||||
- **FlowAction** 让流步骤逻辑高度复用,便于维护和扩展。
|
||||
- 支持多种定义方式,适应不同复杂度的业务场景。
|
||||
- 可通过 `uiSchema` 和 `defaultParams` 配置参数界面和默认值,提升易用性。
|
||||
- 合理使用 `paramsRequired` 和 `hideInSettings`,提升操作安全性和灵活性。
|
||||
|
||||
---
|
||||
|
||||
## 示例:在 Drawer 中使用
|
||||
|
||||
以下示例演示如何在 Drawer 中使用 FlowAction,并传递 `currentDrawer` 到 `sharedContext`。
|
||||
|
||||
```tsx
|
||||
// 1. 先声明 currentDrawer
|
||||
let currentDrawer: any;
|
||||
|
||||
// 2. 定义内容组件,确保 currentDrawer 已赋值
|
||||
function DrawerContent() {
|
||||
return (
|
||||
<div>
|
||||
<FlowPageComponent
|
||||
uid={`${ctx.model.uid}-drawer`}
|
||||
sharedContext={{ ...ctx.extra, currentDrawer }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 打开 Drawer,并赋值 currentDrawer
|
||||
currentDrawer = ctx.globals.drawer.open({
|
||||
title: '命令式 Drawer',
|
||||
width: 800,
|
||||
content: <DrawerContent />,
|
||||
});
|
||||
```
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
## 主要方法
|
||||
|
||||
- **load(uid: string): Promise<FlowModel \| null>**
|
||||
- **findOne(query: Query): Promise<FlowModel \| null>**
|
||||
根据唯一标识符 uid 从远程加载模型数据。
|
||||
|
||||
- **save(model: FlowModel): Promise<any>**
|
||||
@ -19,7 +19,8 @@
|
||||
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||
constructor(private app: Application) {}
|
||||
|
||||
async load(uid: string) {
|
||||
async findOne(query) {
|
||||
const { uid, parentId } = query;
|
||||
// 实现:根据 uid 获取模型
|
||||
return null;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
|
||||
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
|
||||
constructor(private app: Application) {}
|
||||
async load(uid: string) {
|
||||
async findOne({ uid, parentId }) {
|
||||
// implement fetching a model by id
|
||||
return null;
|
||||
}
|
||||
|
@ -6,7 +6,9 @@ import { Button, Tabs } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }>> {
|
||||
class FlowModelRepository
|
||||
implements IFlowModelRepository<FlowModel<{ parent: never; subModels: { tabs: TabFlowModel[] } }>>
|
||||
{
|
||||
get models() {
|
||||
const models = new Map();
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
@ -22,8 +24,12 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
|
||||
return models;
|
||||
}
|
||||
|
||||
async findOne(query) {
|
||||
return this.load(query.uid);
|
||||
}
|
||||
|
||||
// 从本地存储加载模型数据
|
||||
async load(uid: string) {
|
||||
async load({ uid }) {
|
||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||
if (!data) return null;
|
||||
const json: FlowModel = JSON.parse(data);
|
||||
@ -57,7 +63,10 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
|
||||
localStorage.setItem(`flow-model:${subModel.uid}`, JSON.stringify(subModel.serialize()));
|
||||
});
|
||||
} else if (model.subModels[subModelKey] instanceof FlowModel) {
|
||||
localStorage.setItem(`flow-model:${model.subModels[subModelKey].uid}`, JSON.stringify(model.subModels[subModelKey].serialize()));
|
||||
localStorage.setItem(
|
||||
`flow-model:${model.subModels[subModelKey].uid}`,
|
||||
JSON.stringify(model.subModels[subModelKey].serialize()),
|
||||
);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
@ -72,8 +81,7 @@ class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: nev
|
||||
|
||||
class TabFlowModel extends FlowModel {}
|
||||
|
||||
class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }> {
|
||||
|
||||
class HelloFlowModel extends FlowModel<{ parent: never; subModels: { tabs: TabFlowModel[] } }> {
|
||||
addTab(tab: any) {
|
||||
// 使用新的 addSubModel API 添加子模型
|
||||
const model = this.addSubModel('tabs', tab);
|
||||
@ -88,7 +96,7 @@ class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlo
|
||||
items={this.subModels.tabs?.map((tab) => ({
|
||||
key: tab.getProps().key,
|
||||
label: tab.getProps().label,
|
||||
children: tab.render()
|
||||
children: tab.render(),
|
||||
}))}
|
||||
tabBarExtraContent={
|
||||
<Button
|
||||
@ -98,7 +106,7 @@ class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlo
|
||||
use: 'TabFlowModel',
|
||||
uid: tabId,
|
||||
props: { key: tabId, label: `Tab - ${tabId}` },
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Tab
|
||||
@ -134,7 +142,7 @@ class PluginHelloModel extends Plugin {
|
||||
props: { key: 'tab-2', label: 'Tab 2' },
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
});
|
||||
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
|
||||
}
|
||||
|
@ -26,6 +26,26 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
||||
return models;
|
||||
}
|
||||
|
||||
async findOne(query) {
|
||||
const { uid, parentId } = query;
|
||||
if (uid) {
|
||||
return this.load(uid);
|
||||
} else if (parentId) {
|
||||
return this.loadByParentId(parentId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async loadByParentId(parentId: string) {
|
||||
for (const model of this.models.values()) {
|
||||
if (model.parentId == parentId) {
|
||||
console.log('Loading model by parentId:', parentId, model);
|
||||
return this.load(model.uid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从本地存储加载模型数据
|
||||
async load(uid: string) {
|
||||
const data = localStorage.getItem(`flow-model:${uid}`);
|
||||
@ -43,10 +63,12 @@ export class MockFlowModelRepository implements IFlowModelRepository<FlowModel>
|
||||
json.subModels[model.subKey].push(subModel);
|
||||
} else if (model.subType === 'object') {
|
||||
const subModel = await this.load(model.uid);
|
||||
if (subModel) {
|
||||
json.subModels[model.subKey] = subModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('Loading model:', uid, JSON.stringify(json, null, 2));
|
||||
return json;
|
||||
}
|
||||
|
@ -10,12 +10,12 @@
|
||||
import { FlowModelRenderer, useFlowEngine, useFlowModel } from '@nocobase/flow-engine';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
function InternalFlowPage({ uid }) {
|
||||
function InternalFlowPage({ uid, sharedContext }) {
|
||||
const model = useFlowModel(uid);
|
||||
return <FlowModelRenderer model={model} showFlowSettings hideRemoveInSettings />;
|
||||
return <FlowModelRenderer model={model} sharedContext={sharedContext} showFlowSettings hideRemoveInSettings />;
|
||||
}
|
||||
|
||||
export const FlowPage = () => {
|
||||
@ -23,12 +23,13 @@ export const FlowPage = () => {
|
||||
return <FlowPageComponent uid={params.name} />;
|
||||
};
|
||||
|
||||
export const FlowPageComponent = ({ uid }) => {
|
||||
export const FlowPageComponent = (props) => {
|
||||
const { uid, parentId, sharedContext } = props;
|
||||
const flowEngine = useFlowEngine();
|
||||
const { loading } = useRequest(
|
||||
() => {
|
||||
return flowEngine.loadOrCreateModel({
|
||||
uid: uid,
|
||||
const { loading, data } = useRequest(
|
||||
async () => {
|
||||
const options = {
|
||||
uid,
|
||||
use: 'PageFlowModel',
|
||||
subModels: {
|
||||
tabs: [
|
||||
@ -42,14 +43,22 @@ export const FlowPageComponent = ({ uid }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
if (!uid && parentId) {
|
||||
options['async'] = true;
|
||||
options['parentId'] = parentId;
|
||||
options['subKey'] = 'page';
|
||||
options['subType'] = 'object';
|
||||
}
|
||||
const data = await flowEngine.loadOrCreateModel(options);
|
||||
return data;
|
||||
},
|
||||
{
|
||||
refreshDeps: [uid],
|
||||
refreshDeps: [uid || parentId],
|
||||
},
|
||||
);
|
||||
if (loading) {
|
||||
if (loading || !data?.uid) {
|
||||
return <Spin />;
|
||||
}
|
||||
return <InternalFlowPage uid={uid} />;
|
||||
return <InternalFlowPage uid={data.uid} sharedContext={sharedContext} />;
|
||||
};
|
||||
|
46
packages/core/client/src/flow/models/AddNewActionModel.tsx
Normal file
46
packages/core/client/src/flow/models/AddNewActionModel.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import type { ButtonType } from 'antd/es/button';
|
||||
import React from 'react';
|
||||
import { FlowPageComponent } from '../FlowPage';
|
||||
import { ActionModel } from './ActionModel';
|
||||
|
||||
export class AddNewActionModel extends ActionModel {
|
||||
title = 'Add new';
|
||||
}
|
||||
|
||||
AddNewActionModel.registerFlow({
|
||||
key: 'event1',
|
||||
on: {
|
||||
eventName: 'click',
|
||||
},
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let currentDrawer: any;
|
||||
|
||||
function DrawerContent() {
|
||||
return (
|
||||
<div>
|
||||
<FlowPageComponent parentId={ctx.model.uid} sharedContext={{ ...ctx.extra, currentDrawer }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
currentDrawer = ctx.globals.drawer.open({
|
||||
title: '命令式 Drawer',
|
||||
width: 800,
|
||||
content: <DrawerContent />,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -65,7 +65,7 @@ export class FormModel extends BlockFlowModel {
|
||||
/>
|
||||
<FormButtonGroup>
|
||||
{this.mapSubModels('actions', (action) => (
|
||||
<FlowModelRenderer model={action} showFlowSettings />
|
||||
<FlowModelRenderer model={action} showFlowSettings extraContext={{ currentModel: this }} />
|
||||
))}
|
||||
<AddActionButton model={this} subModelBaseClass="ActionModel" />
|
||||
</FormButtonGroup>
|
||||
@ -107,19 +107,22 @@ FormModel.registerFlow({
|
||||
},
|
||||
async handler(ctx, params) {
|
||||
ctx.model.form = ctx.extra.form || createForm();
|
||||
if (ctx.model.collection) {
|
||||
return;
|
||||
}
|
||||
ctx.model.collection = ctx.globals.dataSourceManager.getCollection(params.dataSourceKey, params.collectionName);
|
||||
if (!ctx.model.collection) {
|
||||
ctx.model.collection = ctx.globals.dataSourceManager.getCollection(
|
||||
params.dataSourceKey,
|
||||
params.collectionName,
|
||||
);
|
||||
const resource = new SingleRecordResource();
|
||||
resource.setDataSourceKey(params.dataSourceKey);
|
||||
resource.setResourceName(params.collectionName);
|
||||
resource.setAPIClient(ctx.globals.api);
|
||||
ctx.model.resource = resource;
|
||||
if (ctx.extra.filterByTk) {
|
||||
resource.setFilterByTk(ctx.extra.filterByTk);
|
||||
await resource.refresh();
|
||||
ctx.model.form.setInitialValues(resource.getData());
|
||||
}
|
||||
console.log('FormModel flow context', ctx.shared, ctx.model.getSharedContext());
|
||||
if (ctx.shared.currentRecord) {
|
||||
ctx.model.resource.setFilterByTk(ctx.shared.currentRecord.id);
|
||||
await ctx.model.resource.refresh();
|
||||
ctx.model.form.setInitialValues(ctx.model.resource.getData());
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -25,11 +25,16 @@ SubmitActionModel.registerFlow({
|
||||
steps: {
|
||||
step1: {
|
||||
async handler(ctx, params) {
|
||||
await ctx.model.parent.form.submit();
|
||||
const values = ctx.model.parent.form.values;
|
||||
await ctx.model.parent.resource.save(values);
|
||||
if (ctx.model.parent.dialog) {
|
||||
ctx.model.parent.dialog.close();
|
||||
if (ctx.extra.currentModel) {
|
||||
await ctx.extra.currentModel.form.submit();
|
||||
const values = ctx.extra.currentModel.form.values;
|
||||
await ctx.extra.currentModel.resource.save(values);
|
||||
}
|
||||
if (ctx.shared.currentDrawer) {
|
||||
ctx.shared.currentDrawer.destroy();
|
||||
}
|
||||
if (ctx.shared.currentResource) {
|
||||
ctx.shared.currentResource.refresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -94,11 +94,11 @@ TableColumnModel.define({
|
||||
sort: 0,
|
||||
});
|
||||
|
||||
const Columns = observer<any>(({ record, model }) => {
|
||||
const Columns = observer<any>(({ record, model, index }) => {
|
||||
return (
|
||||
<Space>
|
||||
{model.mapSubModels('actions', (action: ActionModel) => {
|
||||
const fork = action.createFork({}, `${record.id}`);
|
||||
const fork = action.createFork({}, `${index}`);
|
||||
return (
|
||||
<FlowModelRenderer
|
||||
showFlowSettings
|
||||
@ -130,6 +130,13 @@ export class TableActionsColumnModel extends TableColumnModel {
|
||||
model={this}
|
||||
subModelKey={'actions'}
|
||||
items={() => [
|
||||
{
|
||||
key: 'view',
|
||||
label: 'View',
|
||||
createModelOptions: {
|
||||
use: 'ViewActionModel',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'link',
|
||||
label: 'Link',
|
||||
@ -156,7 +163,7 @@ export class TableActionsColumnModel extends TableColumnModel {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (value, record, index) => <Columns record={record} model={this} />;
|
||||
return (value, record, index) => <Columns record={record} model={this} index={index} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,13 @@ export class TableModel extends BlockFlowModel<S> {
|
||||
model={this}
|
||||
subModelKey={'actions'}
|
||||
items={() => [
|
||||
{
|
||||
key: 'addnew',
|
||||
label: 'Add new',
|
||||
createModelOptions: {
|
||||
use: 'AddNewActionModel',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
@ -96,9 +103,7 @@ export class TableModel extends BlockFlowModel<S> {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Button type="primary" icon={<SettingOutlined />}>
|
||||
Configure actions
|
||||
</Button>
|
||||
<Button icon={<SettingOutlined />}>Configure actions</Button>
|
||||
</AddActionModel>
|
||||
</Space>
|
||||
<Table
|
||||
|
@ -27,14 +27,21 @@ ViewActionModel.registerFlow({
|
||||
steps: {
|
||||
step1: {
|
||||
handler(ctx, params) {
|
||||
ctx.globals.drawer.open({
|
||||
// eslint-disable-next-line prefer-const
|
||||
let currentDrawer: any;
|
||||
|
||||
function DrawerContent() {
|
||||
return (
|
||||
<div>
|
||||
<FlowPageComponent parentId={ctx.model.uid} sharedContext={{ ...ctx.extra, currentDrawer }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
currentDrawer = ctx.globals.drawer.open({
|
||||
title: '命令式 Drawer',
|
||||
width: 800,
|
||||
content: (
|
||||
<div>
|
||||
<FlowPageComponent uid={`${ctx.model.uid}-drawer`} />
|
||||
</div>
|
||||
),
|
||||
content: <DrawerContent />,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
export * from './ActionModel';
|
||||
export * from './BulkDeleteActionModel';
|
||||
export * from './AddNewActionModel';
|
||||
export * from './BlockFlowModel';
|
||||
export * from './BlockGridFlowModel';
|
||||
export * from './BulkDeleteActionModel';
|
||||
export * from './CalendarBlockFlowModel';
|
||||
export * from './DeleteActionModel';
|
||||
export * from './FormFieldModel';
|
||||
|
@ -1,3 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
@ -27,7 +36,7 @@
|
||||
*/
|
||||
|
||||
import { observer } from '@formily/reactive-react';
|
||||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, useEffect } from 'react';
|
||||
import { useApplyAutoFlows, useFlowExtraContext } from '../hooks';
|
||||
import { FlowModel } from '../models';
|
||||
import { FlowsContextMenu } from './settings/wrappers/contextual/FlowsContextMenu';
|
||||
@ -51,7 +60,10 @@ interface FlowModelRendererProps {
|
||||
skipApplyAutoFlows?: boolean; // 默认 false
|
||||
|
||||
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的额外上下文 */
|
||||
extraContext?: Record<string, any>
|
||||
extraContext?: Record<string, any>;
|
||||
|
||||
/** Model 共享运行上下文,会沿着 model 树向下传递 */
|
||||
sharedContext?: Record<string, any>;
|
||||
|
||||
/** 是否为每个组件独立执行 auto flow,默认 false */
|
||||
independentAutoFlowExecution?: boolean; // 默认 false
|
||||
@ -66,8 +78,18 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
||||
flowSettingsVariant: string;
|
||||
hideRemoveInSettings: boolean;
|
||||
extraContext?: Record<string, any>;
|
||||
sharedContext?: Record<string, any>;
|
||||
independentAutoFlowExecution?: boolean;
|
||||
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, extraContext, independentAutoFlowExecution }) => {
|
||||
}> = observer(
|
||||
({
|
||||
model,
|
||||
showFlowSettings,
|
||||
flowSettingsVariant,
|
||||
hideRemoveInSettings,
|
||||
extraContext,
|
||||
sharedContext,
|
||||
independentAutoFlowExecution,
|
||||
}) => {
|
||||
const defaultExtraContext = useFlowExtraContext();
|
||||
useApplyAutoFlows(model, extraContext || defaultExtraContext, !independentAutoFlowExecution);
|
||||
|
||||
@ -79,7 +101,8 @@ const FlowModelRendererWithAutoFlows: React.FC<{
|
||||
hideRemoveInSettings={hideRemoveInSettings}
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 内部组件:不带 useApplyAutoFlows 的渲染器
|
||||
@ -89,7 +112,8 @@ const FlowModelRendererWithoutAutoFlows: React.FC<{
|
||||
showFlowSettings: boolean;
|
||||
flowSettingsVariant: string;
|
||||
hideRemoveInSettings: boolean;
|
||||
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings }) => {
|
||||
sharedContext?: Record<string, any>;
|
||||
}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, sharedContext }) => {
|
||||
return (
|
||||
<FlowModelRendererCore
|
||||
model={model}
|
||||
@ -120,10 +144,18 @@ const FlowModelRendererCore: React.FC<{
|
||||
// 根据 flowSettingsVariant 包装相应的设置组件
|
||||
switch (flowSettingsVariant) {
|
||||
case 'dropdown':
|
||||
return <FlowsFloatContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>{modelContent}</FlowsFloatContextMenu>;
|
||||
return (
|
||||
<FlowsFloatContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>
|
||||
{modelContent}
|
||||
</FlowsFloatContextMenu>
|
||||
);
|
||||
|
||||
case 'contextMenu':
|
||||
return <FlowsContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>{modelContent}</FlowsContextMenu>;
|
||||
return (
|
||||
<FlowsContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>
|
||||
{modelContent}
|
||||
</FlowsContextMenu>
|
||||
);
|
||||
|
||||
case 'modal':
|
||||
// TODO: 实现 modal 模式的流程设置
|
||||
@ -136,10 +168,12 @@ const FlowModelRendererCore: React.FC<{
|
||||
return modelContent;
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
`FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`,
|
||||
console.warn(`FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`);
|
||||
return (
|
||||
<FlowsFloatContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>
|
||||
{modelContent}
|
||||
</FlowsFloatContextMenu>
|
||||
);
|
||||
return <FlowsFloatContextMenu model={model} showDeleteButton={!hideRemoveInSettings}>{modelContent}</FlowsFloatContextMenu>;
|
||||
}
|
||||
});
|
||||
|
||||
@ -155,6 +189,7 @@ const FlowModelRendererCore: React.FC<{
|
||||
* @param {boolean} props.hideRemoveInSettings - Whether to hide remove button in settings.
|
||||
* @param {boolean} props.skipApplyAutoFlows - Whether to skip applying auto flows.
|
||||
* @param {any} props.extraContext - Extra context to pass to useApplyAutoFlows when skipApplyAutoFlows is false.
|
||||
* @param {any} props.sharedContext - Shared context to pass to the model.
|
||||
* @param {boolean} props.independentAutoFlowExecution - Whether each component has independent auto flow execution.
|
||||
* @returns {React.ReactNode | null} The rendered output of the model, or null if the model or its render method is invalid.
|
||||
*/
|
||||
@ -166,6 +201,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
||||
hideRemoveInSettings = false,
|
||||
skipApplyAutoFlows = false,
|
||||
extraContext,
|
||||
sharedContext,
|
||||
independentAutoFlowExecution = false,
|
||||
}) => {
|
||||
if (!model || typeof model.render !== 'function') {
|
||||
@ -174,6 +210,10 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
||||
return null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
model.setSharedContext(sharedContext);
|
||||
}, [model, sharedContext]);
|
||||
|
||||
// 根据 skipApplyAutoFlows 选择不同的内部组件
|
||||
if (skipApplyAutoFlows) {
|
||||
return (
|
||||
@ -183,6 +223,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
||||
showFlowSettings={showFlowSettings}
|
||||
flowSettingsVariant={flowSettingsVariant}
|
||||
hideRemoveInSettings={hideRemoveInSettings}
|
||||
sharedContext={sharedContext}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
@ -195,6 +236,7 @@ export const FlowModelRenderer: React.FC<FlowModelRendererProps> = observer(
|
||||
flowSettingsVariant={flowSettingsVariant}
|
||||
hideRemoveInSettings={hideRemoveInSettings}
|
||||
extraContext={extraContext}
|
||||
sharedContext={sharedContext}
|
||||
independentAutoFlowExecution={independentAutoFlowExecution}
|
||||
/>
|
||||
</Suspense>
|
||||
|
@ -13,6 +13,26 @@ import React from 'react';
|
||||
import { ActionStepDefinition } from '../../../../types';
|
||||
import { resolveDefaultParams } from '../../../../utils';
|
||||
|
||||
/**
|
||||
* 检查步骤是否已经有了所需的配置值
|
||||
* @param uiSchema 步骤的 UI Schema
|
||||
* @param currentParams 当前步骤的参数
|
||||
* @returns 是否已经有了所需的配置值
|
||||
*/
|
||||
function hasRequiredParams(uiSchema: Record<string, any>, currentParams: Record<string, any>): boolean {
|
||||
// 检查 uiSchema 中所有 required 为 true 的字段
|
||||
for (const [fieldKey, fieldSchema] of Object.entries(uiSchema)) {
|
||||
if (fieldSchema.required === true) {
|
||||
// 如果字段是必需的,但当前参数中没有值或值为空
|
||||
const value = currentParams[fieldKey];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const SchemaField = createSchemaField();
|
||||
|
||||
/**
|
||||
@ -87,8 +107,16 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
}
|
||||
});
|
||||
|
||||
// 如果有可配置的UI Schema,添加到列表中
|
||||
// 如果有可配置的UI Schema,检查是否已经有了所需的配置值
|
||||
if (Object.keys(mergedUiSchema).length > 0) {
|
||||
// 获取当前步骤的参数
|
||||
const currentStepParams = model.getStepParams(flowKey, stepKey) || {};
|
||||
|
||||
// 检查是否已经有了所需的配置值
|
||||
const hasAllRequiredParams = hasRequiredParams(mergedUiSchema, currentStepParams);
|
||||
|
||||
// 只有当缺少必需参数时才添加到列表中
|
||||
if (!hasAllRequiredParams) {
|
||||
requiredSteps.push({
|
||||
flowKey,
|
||||
stepKey,
|
||||
@ -101,6 +129,7 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有需要配置的步骤,显示提示
|
||||
if (requiredSteps.length === 0) {
|
||||
@ -121,10 +150,16 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
|
||||
for (const { flowKey, stepKey, step } of requiredSteps) {
|
||||
const stepParams = model.getStepParams(flowKey, stepKey) || {};
|
||||
|
||||
// 如果step使用了action,也获取action的defaultParams
|
||||
let actionDefaultParams = {};
|
||||
if (step.use) {
|
||||
const action = model.flowEngine?.getAction?.(step.use);
|
||||
actionDefaultParams = action.defaultParams || {};
|
||||
}
|
||||
// 解析 defaultParams
|
||||
const resolvedActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
|
||||
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
|
||||
const mergedParams = { ...resolvedDefaultParams, ...stepParams };
|
||||
const mergedParams = { ...resolvedActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
|
||||
|
||||
if (Object.keys(mergedParams).length > 0) {
|
||||
if (!initialValues[flowKey]) {
|
||||
@ -137,7 +172,7 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
// 构建分步表单的 Schema
|
||||
const stepPanes: Record<string, any> = {};
|
||||
|
||||
requiredSteps.forEach(({ flowKey, stepKey, uiSchema, title, flowTitle }, index) => {
|
||||
requiredSteps.forEach(({ flowKey, stepKey, uiSchema, title, flowTitle }) => {
|
||||
const stepId = `${flowKey}_${stepKey}`;
|
||||
|
||||
stepPanes[stepId] = {
|
||||
@ -261,7 +296,7 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
formStep.next();
|
||||
}
|
||||
})
|
||||
.catch((errors) => {
|
||||
.catch((errors: any) => {
|
||||
console.log('表单验证失败:', errors);
|
||||
// 可以在这里添加更详细的错误处理
|
||||
});
|
||||
@ -310,7 +345,7 @@ const openRequiredParamsStepFormDialog = async ({
|
||||
formStep.next();
|
||||
}
|
||||
})
|
||||
.catch((errors) => {
|
||||
.catch((errors: any) => {
|
||||
console.log('表单验证失败:', errors);
|
||||
// 可以在这里添加更详细的错误处理
|
||||
});
|
||||
|
@ -55,6 +55,7 @@ const openStepSettingsDialog = async ({
|
||||
// 获取可配置的步骤信息
|
||||
const stepDefinition = step as ActionStepDefinition;
|
||||
const stepUiSchema = stepDefinition.uiSchema || {};
|
||||
let actionDefaultParams = {};
|
||||
|
||||
// 如果step使用了action,也获取action的uiSchema
|
||||
let actionUiSchema = {};
|
||||
@ -63,6 +64,7 @@ const openStepSettingsDialog = async ({
|
||||
if (action && action.uiSchema) {
|
||||
actionUiSchema = action.uiSchema;
|
||||
}
|
||||
actionDefaultParams = action.defaultParams || {};
|
||||
}
|
||||
|
||||
// 合并uiSchema,确保step的uiSchema优先级更高
|
||||
@ -93,7 +95,8 @@ const openStepSettingsDialog = async ({
|
||||
|
||||
// 解析 defaultParams
|
||||
const resolvedDefaultParams = await resolveDefaultParams(stepDefinition.defaultParams, paramsContext);
|
||||
const initialValues = { ...resolvedDefaultParams, ...stepParams };
|
||||
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
|
||||
const initialValues = { ...resolveActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
|
||||
|
||||
// 构建表单Schema
|
||||
const formSchema: ISchema = {
|
||||
|
@ -8,10 +8,11 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { AddSubModelButton } from './AddSubModelButton';
|
||||
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton';
|
||||
import { FlowModel } from '../../models/flowModel';
|
||||
import { ModelConstructor } from '../../types';
|
||||
import { Button } from 'antd';
|
||||
import { createBlockItems } from './blockItems';
|
||||
|
||||
interface AddBlockButtonProps {
|
||||
/**
|
||||
@ -32,16 +33,43 @@ interface AddBlockButtonProps {
|
||||
* 按钮文本
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
/**
|
||||
* 自定义 items(如果提供,将覆盖默认的数据源选择行为)
|
||||
*/
|
||||
items?: SubModelItemsType;
|
||||
/**
|
||||
* 过滤区块类型的函数
|
||||
*/
|
||||
filterBlocks?: (blockClass: ModelConstructor, className: string) => boolean;
|
||||
/**
|
||||
* 追加额外的菜单项到默认菜单中
|
||||
*/
|
||||
appendItems?: SubModelItemsType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门用于添加块模型的按钮组件
|
||||
*
|
||||
* 提供类似 page:addBlock 的区块类型 -> 数据源 -> 数据表的层级结构
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 基本用法
|
||||
* <AddBlockButton
|
||||
* model={parentModel}
|
||||
* subModelBaseClass={'FlowModel'}
|
||||
* subModelBaseClass={'BlockFlowModel'}
|
||||
* />
|
||||
*
|
||||
* // 追加自定义菜单项
|
||||
* <AddBlockButton
|
||||
* model={parentModel}
|
||||
* appendItems={[
|
||||
* {
|
||||
* key: 'customBlock',
|
||||
* label: 'Custom Block',
|
||||
* createModelOptions: { use: 'CustomBlock' }
|
||||
* }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
@ -51,31 +79,33 @@ export const AddBlockButton: React.FC<AddBlockButtonProps> = ({
|
||||
subModelKey = 'blocks',
|
||||
children = <Button>Add block</Button>,
|
||||
subModelType = 'array',
|
||||
items,
|
||||
filterBlocks,
|
||||
appendItems,
|
||||
onModelAdded,
|
||||
}) => {
|
||||
const items = useMemo(() => {
|
||||
const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
|
||||
const registeredBlocks = [];
|
||||
for (const [className, ModelClass] of blockClasses) {
|
||||
registeredBlocks.push({
|
||||
key: className,
|
||||
label: ModelClass.meta?.title || className,
|
||||
icon: ModelClass.meta?.icon,
|
||||
createModelOptions: {
|
||||
...ModelClass.meta?.defaultOptions,
|
||||
use: className,
|
||||
},
|
||||
});
|
||||
// 确定最终使用的 items
|
||||
const finalItems = useMemo(() => {
|
||||
if (items) {
|
||||
// 如果明确提供了 items,直接使用
|
||||
return items;
|
||||
}
|
||||
return registeredBlocks;
|
||||
}, [model, subModelBaseClass]);
|
||||
|
||||
// 创建区块菜单项,并合并追加的 items
|
||||
const blockItems = createBlockItems(model, {
|
||||
subModelBaseClass,
|
||||
filterBlocks,
|
||||
});
|
||||
|
||||
return mergeSubModelItems([blockItems, appendItems]);
|
||||
}, [items, model, subModelBaseClass, filterBlocks, appendItems]);
|
||||
|
||||
return (
|
||||
<AddSubModelButton
|
||||
model={model}
|
||||
subModelKey={subModelKey}
|
||||
subModelType={subModelType}
|
||||
items={items}
|
||||
items={finalItems}
|
||||
onModelAdded={onModelAdded}
|
||||
>
|
||||
{children}
|
||||
|
189
packages/core/flow-engine/src/components/subModel/blockItems.ts
Normal file
189
packages/core/flow-engine/src/components/subModel/blockItems.ts
Normal file
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { FlowModel } from '../../models/flowModel';
|
||||
import { ModelConstructor } from '../../types';
|
||||
import { SubModelItem, SubModelItemsType } from './AddSubModelButton';
|
||||
import { DataSource, DataSourceManager, Collection } from '../../data-source';
|
||||
|
||||
export interface BlockItemsOptions {
|
||||
/**
|
||||
* 子模型基类,用于确定支持的区块类型
|
||||
*/
|
||||
subModelBaseClass?: string | ModelConstructor;
|
||||
/**
|
||||
* 过滤区块类型的函数
|
||||
*/
|
||||
filterBlocks?: (blockClass: ModelConstructor, className: string) => boolean;
|
||||
/**
|
||||
* 自定义区块选项
|
||||
*/
|
||||
customBlocks?: SubModelItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据源和数据表信息
|
||||
* 从 flowEngine 的全局上下文获取数据源管理器信息
|
||||
*/
|
||||
async function getDataSourcesWithCollections(model: FlowModel) {
|
||||
try {
|
||||
// 从 flowEngine 的全局上下文获取数据源管理器
|
||||
const globalContext = model.flowEngine.getContext();
|
||||
const dataSourceManager: DataSourceManager = globalContext?.dataSourceManager;
|
||||
|
||||
if (!dataSourceManager) {
|
||||
// 如果没有数据源管理器,返回空数组
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取所有数据源
|
||||
const allDataSources: DataSource[] = dataSourceManager.getDataSources();
|
||||
|
||||
// 转换为我们需要的格式
|
||||
return allDataSources.map((dataSource: DataSource) => {
|
||||
const key = dataSource.name;
|
||||
const displayName = dataSource.options.displayName || dataSource.name;
|
||||
|
||||
// 从 collectionManager 获取 collections
|
||||
const collections: Collection[] = dataSource.getCollections();
|
||||
|
||||
return {
|
||||
key,
|
||||
displayName,
|
||||
collections: collections.map((collection: Collection) => ({
|
||||
name: collection.name,
|
||||
title: collection.title,
|
||||
dataSource: key,
|
||||
})),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to get data sources:', error);
|
||||
// 返回空数组,不提供假数据
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建区块菜单项的工具函数
|
||||
*
|
||||
* 生成包含数据区块和其他区块的完整菜单结构:
|
||||
* - 数据区块:区块类型 → 数据源 → 数据表的层级结构
|
||||
* - 其他区块:平铺的区块列表
|
||||
*
|
||||
* @param model FlowModel 实例
|
||||
* @param options 配置选项
|
||||
* @returns SubModelItemsType 格式的菜单项
|
||||
*/
|
||||
export function createBlockItems(model: FlowModel, options: BlockItemsOptions = {}): SubModelItemsType {
|
||||
const { subModelBaseClass = 'BlockFlowModel', filterBlocks, customBlocks = [] } = options;
|
||||
|
||||
// 获取所有注册的区块类
|
||||
const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass);
|
||||
|
||||
// 分类区块:数据区块 vs 其他区块
|
||||
const dataBlocks: Array<{ className: string; ModelClass: ModelConstructor }> = [];
|
||||
const otherBlocks: Array<{ className: string; ModelClass: ModelConstructor }> = [];
|
||||
|
||||
for (const [className, ModelClass] of blockClasses) {
|
||||
// 应用过滤器
|
||||
if (filterBlocks && !filterBlocks(ModelClass, className)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判断是否为数据区块
|
||||
const meta = (ModelClass as any).meta;
|
||||
const isDataBlock =
|
||||
meta?.category === 'data' ||
|
||||
meta?.requiresDataSource === true ||
|
||||
className.toLowerCase().includes('table') ||
|
||||
className.toLowerCase().includes('form') ||
|
||||
className.toLowerCase().includes('details') ||
|
||||
className.toLowerCase().includes('list') ||
|
||||
className.toLowerCase().includes('grid');
|
||||
|
||||
if (isDataBlock) {
|
||||
dataBlocks.push({ className, ModelClass });
|
||||
} else {
|
||||
otherBlocks.push({ className, ModelClass });
|
||||
}
|
||||
}
|
||||
|
||||
const result: SubModelItem[] = [];
|
||||
|
||||
// 数据区块分组
|
||||
if (dataBlocks.length > 0) {
|
||||
result.push({
|
||||
key: 'dataBlocks',
|
||||
label: 'Data blocks',
|
||||
type: 'group',
|
||||
children: async () => {
|
||||
const dataSources = await getDataSourcesWithCollections(model);
|
||||
|
||||
// 按区块类型组织菜单:区块 → 数据源 → 数据表
|
||||
return dataBlocks.map(({ className, ModelClass }) => {
|
||||
const meta = (ModelClass as any).meta;
|
||||
return {
|
||||
key: className,
|
||||
label: meta?.title || className,
|
||||
icon: meta?.icon,
|
||||
children: dataSources.map((dataSource) => ({
|
||||
key: `${className}.${dataSource.key}`,
|
||||
label: dataSource.displayName,
|
||||
children: dataSource.collections.map((collection) => ({
|
||||
key: `${className}.${dataSource.key}.${collection.name}`,
|
||||
label: collection.title || collection.name,
|
||||
createModelOptions: {
|
||||
...meta?.defaultOptions,
|
||||
use: className,
|
||||
stepParams: {
|
||||
default: {
|
||||
step1: {
|
||||
dataSourceKey: dataSource.key,
|
||||
collectionName: collection.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})),
|
||||
})),
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 其他区块分组
|
||||
if (otherBlocks.length > 0 || customBlocks.length > 0) {
|
||||
const otherBlockItems = [
|
||||
...otherBlocks.map(({ className, ModelClass }) => {
|
||||
const meta = (ModelClass as any).meta;
|
||||
return {
|
||||
key: className,
|
||||
label: meta?.title || className,
|
||||
icon: meta?.icon,
|
||||
createModelOptions: {
|
||||
...meta?.defaultOptions,
|
||||
use: className,
|
||||
},
|
||||
};
|
||||
}),
|
||||
...customBlocks,
|
||||
];
|
||||
|
||||
result.push({
|
||||
key: 'otherBlocks',
|
||||
label: 'Other blocks',
|
||||
type: 'group',
|
||||
children: otherBlockItems,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -12,4 +12,5 @@ export * from './AddBlockButton';
|
||||
export * from './AddFieldButton';
|
||||
export * from './AddSubModel';
|
||||
export * from './AddSubModelButton';
|
||||
export * from './blockItems';
|
||||
//
|
||||
|
@ -227,13 +227,13 @@ export class FlowEngine {
|
||||
|
||||
async loadModel<T extends FlowModel = FlowModel>(uid: string): Promise<T | null> {
|
||||
if (!this.ensureModelRepository()) return;
|
||||
const data = await this.modelRepository.load(uid);
|
||||
const data = await this.modelRepository.findOne({ uid });
|
||||
return data?.uid ? this.createModel<T>(data as any) : null;
|
||||
}
|
||||
|
||||
async loadOrCreateModel<T extends FlowModel = FlowModel>(options): Promise<T | null> {
|
||||
if (!this.ensureModelRepository()) return;
|
||||
const data = await this.modelRepository.load(options.uid);
|
||||
const data = await this.modelRepository.findOne(options);
|
||||
if (data?.uid) {
|
||||
return this.createModel<T>(data as any);
|
||||
} else {
|
||||
|
@ -14,7 +14,6 @@ import { uid } from 'uid/secure';
|
||||
import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog';
|
||||
import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog';
|
||||
import { FlowEngine } from '../flowEngine';
|
||||
import { resolveDefaultParams } from '../utils';
|
||||
import type {
|
||||
ActionStepDefinition,
|
||||
ArrayElementType,
|
||||
@ -30,7 +29,7 @@ import type {
|
||||
StepParams,
|
||||
} from '../types';
|
||||
import { ExtendedFlowDefinition, FlowExtraContext, IModelComponentProps, ReadonlyModelProps } from '../types';
|
||||
import { generateUid, mergeFlowDefinitions } from '../utils';
|
||||
import { FlowExitException, generateUid, mergeFlowDefinitions, resolveDefaultParams } from '../utils';
|
||||
import { ForkFlowModel } from './forkFlowModel';
|
||||
|
||||
// 使用WeakMap存储每个类的meta
|
||||
@ -47,6 +46,8 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
public flowEngine: FlowEngine;
|
||||
public parent: Structure['parent'];
|
||||
public subModels: Structure['subModels'];
|
||||
private _options: FlowModelOptions<Structure>;
|
||||
|
||||
/**
|
||||
* 所有 fork 实例的引用集合。
|
||||
* 使用 Set 便于在销毁时主动遍历并调用 dispose,避免悬挂引用。
|
||||
@ -57,20 +58,26 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
* 基于 key 的 fork 实例缓存,用于复用 fork 实例
|
||||
*/
|
||||
private forkCache: Map<string, ForkFlowModel<any>> = new Map();
|
||||
// public static meta: FlowModelMeta;
|
||||
// model 树的共享运行上下文
|
||||
private _sharedContext: Record<string, any> = {};
|
||||
|
||||
constructor(protected options: FlowModelOptions<Structure>) {
|
||||
constructor(options: FlowModelOptions<Structure>) {
|
||||
if (options?.flowEngine?.getModel(options.uid)) {
|
||||
// 此时 new FlowModel 并不创建新实例,而是返回已存在的实例,避免重复创建同一个model实例
|
||||
return options.flowEngine.getModel(options.uid);
|
||||
}
|
||||
|
||||
this.uid = options.uid || uid();
|
||||
if (!options.uid) {
|
||||
options.uid = uid();
|
||||
}
|
||||
|
||||
this.uid = options.uid;
|
||||
this.props = options.props || {};
|
||||
this.stepParams = options.stepParams || {};
|
||||
this.subModels = {};
|
||||
this.flowEngine = options.flowEngine;
|
||||
this.sortIndex = options.sortIndex || 0;
|
||||
this._options = options;
|
||||
|
||||
define(this, {
|
||||
props: observable,
|
||||
@ -343,9 +350,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
}
|
||||
|
||||
let lastResult: any;
|
||||
let exited = false;
|
||||
const stepResults: Record<string, any> = {};
|
||||
const shared: Record<string, any> = {};
|
||||
|
||||
// Create a new FlowContext instance for this flow execution
|
||||
const createLogger = (level: string) => (message: string, meta?: any) => {
|
||||
@ -357,8 +362,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
const globalContexts = currentFlowEngine.getContext() || {};
|
||||
const flowContext: FlowContext<this> = {
|
||||
exit: () => {
|
||||
exited = true;
|
||||
console.log(`Flow '${flowKey}' on model '${this.uid}' exited via ctx.exit().`);
|
||||
throw new FlowExitException(flowKey, this.uid);
|
||||
},
|
||||
logger: {
|
||||
info: createLogger('INFO'),
|
||||
@ -367,7 +371,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
debug: createLogger('DEBUG'),
|
||||
},
|
||||
stepResults,
|
||||
shared,
|
||||
shared: this.getSharedContext(),
|
||||
globals: globalContexts,
|
||||
extra: extra || {},
|
||||
model: this,
|
||||
@ -377,8 +381,6 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
for (const stepKey in flow.steps) {
|
||||
if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) {
|
||||
const step: StepDefinition = flow.steps[stepKey];
|
||||
if (exited) break;
|
||||
|
||||
let handler: ((ctx: FlowContext<this>, params: any) => Promise<any> | any) | undefined;
|
||||
let combinedParams: Record<string, any> = {};
|
||||
let actionDefinition;
|
||||
@ -424,12 +426,18 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
// Store step result
|
||||
stepResults[stepKey] = lastResult;
|
||||
} catch (error) {
|
||||
// 检查是否是通过 ctx.exit() 正常退出
|
||||
if (error instanceof FlowExitException) {
|
||||
console.log(`[FlowEngine] ${error.message}`);
|
||||
return Promise.resolve(stepResults);
|
||||
}
|
||||
|
||||
console.error(`BaseModel.applyFlow: Error executing step '${stepKey}' in flow '${flowKey}':`, error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(lastResult);
|
||||
return Promise.resolve(stepResults);
|
||||
}
|
||||
|
||||
dispatchEvent(eventName: string, extra?: FlowExtraContext): void {
|
||||
@ -542,7 +550,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
throw new Error('Parent must be an instance of FlowModel.');
|
||||
}
|
||||
this.parent = parent;
|
||||
this.options.parentId = parent.uid;
|
||||
this._options.parentId = parent.uid;
|
||||
}
|
||||
|
||||
addSubModel(subKey: string, options: CreateModelOptions | FlowModel) {
|
||||
@ -718,11 +726,22 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
|
||||
});
|
||||
}
|
||||
|
||||
public setSharedContext(ctx: Record<string, any>) {
|
||||
this._sharedContext = ctx;
|
||||
}
|
||||
|
||||
public getSharedContext() {
|
||||
return {
|
||||
...this.parent?.getSharedContext(),
|
||||
...this._sharedContext, // 当前实例的 context 优先级最高
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: 不完整,需要考虑 sub-model 的情况
|
||||
serialize(): Record<string, any> {
|
||||
const data = {
|
||||
uid: this.uid,
|
||||
..._.omit(this.options, ['flowEngine']),
|
||||
..._.omit(this._options, ['flowEngine']),
|
||||
props: this.props,
|
||||
stepParams: this.stepParams,
|
||||
sortIndex: this.sortIndex,
|
||||
|
@ -248,7 +248,7 @@ export interface CreateModelOptions {
|
||||
[key: string]: any; // 允许额外的自定义选项
|
||||
}
|
||||
export interface IFlowModelRepository<T extends FlowModel = FlowModel> {
|
||||
load(uid: string): Promise<Record<string, any> | null>;
|
||||
findOne(query: Record<string, any>): Promise<Record<string, any> | null>;
|
||||
save(model: T): Promise<Record<string, any>>;
|
||||
destroy(uid: string): Promise<boolean>;
|
||||
}
|
||||
|
@ -102,3 +102,19 @@ export async function resolveDefaultParams<TModel extends FlowModel = FlowModel>
|
||||
|
||||
return defaultParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程正常退出异常类
|
||||
* 用于标识通过 ctx.exit() 正常退出的情况
|
||||
*/
|
||||
export class FlowExitException extends Error {
|
||||
public readonly flowKey: string;
|
||||
public readonly modelUid: string;
|
||||
|
||||
constructor(flowKey: string, modelUid: string, message?: string) {
|
||||
super(message || `Flow '${flowKey}' on model '${modelUid}' exited via ctx.exit().`);
|
||||
this.name = 'FlowExitException';
|
||||
this.flowKey = flowKey;
|
||||
this.modelUid = modelUid;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user