Merge event-filter

This commit is contained in:
xilesun 2025-06-12 11:50:49 +08:00
commit b80d4569b5
183 changed files with 18024 additions and 1381 deletions

View File

@ -54,7 +54,6 @@
"react-dom": "^18.0.0",
"nwsapi": "2.2.7",
"antd": "5.24.2",
"@formily/antd-v5": "1.2.3",
"dayjs": "1.11.13",
"@ant-design/icons": "^5.6.1"
},

View File

@ -64,10 +64,6 @@ export default defineConfig({
title: 'Application',
link: '/core/application/application',
},
{
title: 'Plugin',
link: '/core/application/plugin',
},
{
title: 'PluginManager',
link: '/core/application/plugin-manager',
@ -86,6 +82,165 @@ export default defineConfig({
},
],
},
{
title: 'FlowEngine',
type: 'group',
children: [
{
title: 'Overview',
link: '/core/flow-engine',
},
{
title: 'FlowEngine',
link: '/core/flow-engine/flow-engine',
},
{
title: 'FlowModelRepository',
link: '/core/flow-engine/flow-model-repository',
},
{
title: 'FlowModel',
link: '/core/flow-engine/flow-model',
},
{
title: 'FlowModelRenderer',
link: '/core/flow-engine/flow-model-renderer',
},
{
title: 'FlowModelSettings',
link: '/core/flow-engine/flow-model-settings',
},
{
title: 'FlowDefinition',
link: '/core/flow-engine/flow-definition',
},
{
title: 'FlowResource',
link: '/core/flow-engine/flow-resource',
},
{
title: 'FlowContext',
link: '/core/flow-engine/flow-context',
},
{
title: 'FlowAction',
link: '/core/flow-engine/flow-action',
},
{
title: 'FlowHooks',
link: '/core/flow-engine/flow-hooks',
},
],
},
{
title: 'Flow Models',
type: 'group',
children: [
{
title: 'Quickstart',
link: '/core/flow-models/quickstart',
},
{
title: 'Overview',
link: '/core/flow-models',
},
{
title: 'LayoutModel',
link: '/core/flow-models/layout-flow-model',
},
{
title: 'LayoutRouteModel',
link: '/core/flow-models/layout-route-flow-model',
},
{
title: 'PageModel',
link: '/core/flow-models/page-flow-model',
},
{
title: 'PageTabModel',
link: '/core/flow-models/page-tab-flow-model',
},
{
title: 'GridModel',
link: '/core/flow-models/grid-flow-model',
},
{
title: 'BlockGridModel',
link: '/core/flow-models/block-grid-flow-model',
},
{
title: 'BlockModel',
link: '/core/flow-models/block-flow-model',
},
{
title: 'FormModel',
link: '/core/flow-models/form-flow-model',
},
{
title: 'TableModel',
link: '/core/flow-models/table-flow-model',
},
{
title: 'DetailsModel',
link: '/core/flow-models/details-flow-model',
},
{
title: 'ListModel',
link: '/core/flow-models/list-flow-model',
},
{
title: 'CalendarModel',
link: '/core/flow-models/calendar-flow-model',
},
{
title: 'KanbanModel',
link: '/core/flow-models/kanban-flow-model',
},
{
title: 'MapModel',
link: '/core/flow-models/map-flow-model',
},
{
title: 'GanttModel',
link: '/core/flow-models/gantt-flow-model',
},
{
title: 'ChartModel',
link: '/core/flow-models/chart-flow-model',
},
{
title: 'MarkdownModel',
link: '/core/flow-models/markdown-flow-model',
},
{
title: 'HtmlModel',
link: '/core/flow-models/html-flow-model',
},
{
title: 'iframeModel',
link: '/core/flow-models/iframe-flow-model',
},
{
title: 'TimelineModel',
link: '/core/flow-models/timeline-flow-model',
},
{
title: 'CollapseModel',
link: '/core/flow-models/collapse-flow-model',
},
],
},
{
title: 'Flow Actions',
type: 'group',
children: [
{
title: 'Overview',
link: '/core/flow-actions',
},
],
},
{
title: 'UI Schema',
type: 'group',

View File

@ -0,0 +1,910 @@
// import React, { useState } from 'react';
// import { Table, Button, Space, Card, App, Flex } from 'antd';
// import Icon, { RedoOutlined, EyeOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
// import {
// EventBus,
// EventFlowManager,
// FilterFlowManager,
// IFilter,
// useApplyFilters,
// BlockConfigsProvider,
// SchemaComponent,
// SchemaComponentProvider,
// SchemaSettings,
// useCompile,
// useBlockConfigs,
// FilterHandlerContext,
// BaseFlowModel,
// FilterFlowProvider
// } from '@nocobase/client';
// import _ from 'lodash';
// import { configureAction } from './actions/open-configure-dialog';
// // 创建事件总线和事件/过滤流管理器
// const eventBus = new EventBus();
// const eventFlowManager = new EventFlowManager(eventBus);
// const filterFlowManager = new FilterFlowManager();
// // --- Mock 数据 ---
// const mockData = [
// { id: 1, name: '张三', age: 30, email: 'zhangsan@example.com' },
// { id: 2, name: '李四', age: 25, email: 'lisi@example.com' },
// { id: 3, name: '王五', age: 35, email: 'wangwu@example.com' },
// { id: 4, name: '赵六', age: 28, email: 'zhaoliu@example.com' },
// { id: 5, name: '钱七', age: 22, email: 'qianqi@example.com' },
// { id: 6, name: '孙八', age: 23, email: 'sunba@example.com' },
// { id: 7, name: '周九', age: 24, email: 'zhoujiu@example.com' },
// { id: 8, name: '吴十', age: 25, email: 'wushi@example.com' },
// { id: 9, name: '郑十一', age: 26, email: 'zhengshi@example.com' },
// { id: 10, name: '王十二', age: 27, email: 'wangshi@example.com' },
// { id: 11, name: '冯十三', age: 28, email: 'fengshi@example.com' },
// { id: 12, name: '陈十四', age: 29, email: 'chenshi@example.com' },
// { id: 13, name: '褚十五', age: 30, email: 'chushi@example.com' },
// { id: 14, name: '卫十六', age: 31, email: 'weishi@example.com' },
// { id: 15, name: '蒋十七', age: 32, email: 'jiangshi@example.com' },
// { id: 16, name: '沈十八', age: 33, email: 'shenshi@example.com' },
// ];
// const defaultEventConfigs: {
// event: string,
// filterSteps: Record<string, any>,
// eventSteps: Record<string, any>
// }[] = [
// {
// event: 'block:demo:event:refresh',
// filterSteps: {
// 'block:demo:action': {
// 'block:demo:action:options': {
// text: '刷新',
// icon: 'RedoOutlined',
// type: 'primary',
// size: 'small'
// }
// }
// },
// eventSteps: {
// 'refreshFlow': {
// 'step-refresh': {
// messageOnSuccess: '数据已成功刷新!',
// showNotification: true
// }
// }
// }
// },
// {
// event: 'block:demo:event:view',
// filterSteps: {
// 'block:demo:action': {
// 'block:demo:action:options': {
// text: '查看',
// icon: 'EyeOutlined',
// type: 'primary',
// size: 'small'
// }
// }
// },
// eventSteps: {
// 'viewFlow': {
// 'step-view': {
// messageOnSuccess: '查看记录详情'
// }
// }
// }
// },
// {
// event: 'block:demo:event:create',
// filterSteps: {
// 'block:demo:action': {
// 'block:demo:action:options': {
// text: '新建',
// icon: 'PlusOutlined',
// type: 'primary',
// size: 'small'
// }
// }
// },
// eventSteps: {
// 'createFlow': {
// 'step-create': {
// messageOnSuccess: '新建记录'
// }
// }
// }
// },
// {
// event: 'block:demo:event:delete',
// filterSteps: {
// 'block:demo:action': {
// 'block:demo:action:options': {
// text: '删除',
// icon: 'DeleteOutlined',
// type: 'primary',
// size: 'small'
// }
// }
// },
// eventSteps: {
// 'deleteFlow': {
// 'step-delete': {
// messageOnSuccess: '删除记录'
// }
// }
// }
// }
// ]
// // --- Mock blockConfigs ---
// const mockBlockConfigs = {
// key: 'demo-block-id',
// blockType: 'demoTable',
// configData: {
// filterSteps: {
// 'block:demo:table': {
// 'block:common:linkages': {
// rule: ''
// },
// 'block:common:fields': {
// fields: ['id', 'name', 'age', 'email']
// },
// 'block:demo:actions': {
// actions: {
// toolbar: [
// { key: 'action-refresh-id' },
// { key: 'action-delete-id' }
// ],
// row: [
// { key: 'action-view-id' }
// ]
// }
// },
// 'block:common:data': {
// collectionName: 'users',
// pageSize: 5,
// page: 1
// }
// }
// },
// eventSteps: {
// 'refreshFlow': {
// 'step-refresh': {
// messageOnSuccess: '数据已成功刷新!'
// }
// },
// 'viewFlow': {
// 'step-view': {
// messageOnSuccess: '查看记录详情'
// }
// },
// 'createFlow': {
// 'step-create': {
// messageOnSuccess: '新建记录'
// }
// }
// }
// },
// actionConfigs: {
// 'action-refresh-id': {
// event: 'block:demo:event:refresh',
// filterSteps: {
// 'action:demo:toolbar': {
// 'action:demo:toolbar:options': {
// text: '刷新',
// icon: 'RedoOutlined',
// buttonType: 'default',
// size: 'small'
// },
// 'block:common:linkages': {
// rule: ''
// },
// 'action:demo:tirgger': {
// 'on': 'onClick'
// },
// }
// },
// eventSteps: {
// 'refreshFlow': {
// 'step-refresh': {
// messageOnSuccess: '数据已成功刷新!',
// showNotification: true
// }
// }
// }
// },
// 'action-delete-id': {
// event: 'block:demo:event:delete',
// filterSteps: {
// 'action:demo:toolbar': {
// 'action:demo:toolbar:options': {
// text: '删除',
// icon: 'DeleteOutlined',
// buttonType: 'primary',
// size: 'small',
// danger: true
// },
// }
// },
// eventSteps: {
// 'deleteFlow': {
// 'step-delete': {
// messageOnSuccess: '删除记录'
// }
// }
// }
// },
// 'action-view-id': {
// event: 'block:demo:event:view',
// filterSteps: {},
// eventSteps: {}
// }
// }
// };
// // 模拟API请求
// const mockApiClient = {
// resource: (resourceName) => ({
// get: async ({ filterByTk }) => {
// // 模拟获取区块配置
// if (resourceName === 'blockConfigs') {
// await new Promise(resolve => setTimeout(resolve, 100)); // 模拟网络延迟
// return {
// data: {
// data: mockBlockConfigs
// }
// };
// }
// return { data: null };
// },
// list: async (params = {}) => {
// // 模拟获取数据列表
// if (resourceName === 'users') {
// await new Promise(resolve => setTimeout(resolve, 300)); // 模拟网络延迟
// const { page = 1, pageSize = 10 } = params as { page?: number; pageSize?: number };
// const startIndex = (page - 1) * pageSize;
// const endIndex = startIndex + pageSize;
// const data = mockData.slice(startIndex, endIndex);
// return {
// data: {
// data,
// meta: {
// count: mockData.length,
// page,
// pageSize
// }
// }
// };
// }
// return { data: null };
// }
// })
// };
// // --- 定义事件 ---
// // 刷新表格事件
// eventFlowManager.addEvent({
// name: 'block:demo:refresh',
// title: '刷新表格数据',
// group: 'data',
// uiSchema: {},
// });
// // 查看记录事件
// eventFlowManager.addEvent({
// name: 'block:demo:view',
// title: '查看记录详情',
// group: 'data',
// uiSchema: {},
// });
// // 新增记录事件
// eventFlowManager.addEvent({
// name: 'block:demo:create',
// title: '新增记录',
// group: 'data',
// uiSchema: {},
// });
// // --- 定义事件流 ---
// eventFlowManager.addFlow({
// key: 'refreshFlow',
// title: '刷新数据流程',
// on: {
// event: 'block:demo:refresh',
// title: '当刷新按钮被点击时',
// },
// steps: [
// {
// key: 'step-refresh',
// action: 'refreshData',
// title: '刷新数据',
// },
// ],
// });
// eventFlowManager.addFlow({
// key: 'viewFlow',
// title: '查看记录流程',
// on: {
// event: 'block:demo:view',
// title: '当查看按钮被点击时',
// },
// steps: [
// {
// key: 'step-view',
// action: 'viewRecord',
// title: '查看记录',
// },
// ],
// });
// eventFlowManager.addFlow({
// key: 'createFlow',
// title: '新增记录流程',
// on: {
// event: 'block:demo:create',
// title: '当新增按钮被点击时',
// },
// steps: [
// {
// key: 'step-create',
// action: 'createRecord',
// title: '新增记录',
// },
// ],
// });
// // --- 定义 Action ---
// eventFlowManager.addAction({
// name: 'refreshData',
// title: '获取数据',
// group: 'data',
// handler: async (params, ctx) => {
// if (ctx.payload?.refresh) {
// await ctx.payload.refresh();
// }
// },
// uiSchema: {},
// });
// eventFlowManager.addAction({
// name: 'viewRecord',
// title: '查看记录',
// group: 'data',
// handler: async (params, ctx) => {
// console.log('查看记录:', ctx.payload?.record);
// ctx.payload?.hooks.message.info(`查看记录 ID: ${ctx.payload?.record.id}`);
// },
// uiSchema: {},
// });
// eventFlowManager.addAction({
// name: 'createRecord',
// title: '新增记录',
// group: 'data',
// handler: async (params, ctx) => {
// console.log('新增记录');
// },
// uiSchema: {},
// });
// // 定义配置相关事件
// // 配置
// eventFlowManager.addEvent({
// name: 'block:configure:click',
// title: '配置参数',
// group: 'configure',
// uiSchema: {},
// });
// eventFlowManager.addFlow({
// key: 'block:demo:configure',
// title: '配置流程',
// on: {
// event: 'block:configure:click',
// title: '当配置按钮被点击时',
// },
// steps: [
// {
// key: 'step-configure',
// title: '配置消息参数',
// action: 'configureAction',
// isAwait: true,
// },
// ],
// });
// eventFlowManager.addAction(configureAction);
// // --- 定义过滤器 ---
// // linkages过滤器
// const linkagesFilter: IFilter<BaseFlowModel> = {
// name: 'block:common:linkages',
// group: 'block',
// title: '联动规则',
// uiSchema: {
// rule: {
// type: 'string',
// title: '联动规则',
// 'x-component': 'Input',
// 'x-component-props': {
// placeholder: '值设置为{{true}}时,表格将隐藏'
// }
// }
// },
// handler: (model, params, context) => {
// const compile = context.payload?.compile;
// if (compile && compile(params?.rule) === true) {
// model.setProps('$break', true);
// }
// },
// };
// // 获取字段配置
// const fieldsFilter: IFilter<BaseFlowModel> = {
// name: 'block:common:fields',
// group: 'block',
// title: '获取字段配置',
// uiSchema: {
// fields: {
// type: 'array',
// title: '显示列',
// 'x-component': 'Select',
// 'x-component-props': {
// mode: 'multiple',
// options: [
// { label: 'ID', value: 'id' },
// { label: '姓名', value: 'name' },
// { label: '年龄', value: 'age' },
// { label: '邮箱', value: 'email' },
// ],
// },
// },
// },
// handler: (model, params, context) => {
// model.setProps('fields', params?.fields || []);
// },
// };
// // 获取操作配置
// const actionsFilter: IFilter<BaseFlowModel> = {
// name: 'block:demo:actions',
// group: 'block',
// title: '获取操作配置',
// uiSchema: {},
// handler: (model, params, context) => {
// const { toolbar, row } = params?.actions || { toolbar: [], row: [] };
// const actionConfigs = model.getProps().actionConfigs || {};
// model.setProps('actions', {
// toolbar: toolbar.map(action => {
// return {
// ...actionConfigs[action.key]
// };
// }),
// row: row.map(action => {
// return {
// ...actionConfigs[action.key]
// };
// })
// });
// },
// };
// // 获取数据
// const dataFilter: IFilter<BaseFlowModel> = {
// name: 'block:common:data',
// group: 'block',
// title: '获取数据',
// uiSchema: {
// pageSize: {
// type: 'number',
// title: '默认每页条数',
// enum: [5, 10, 20, 30, 40, 50],
// 'x-component': 'Select',
// 'x-component-props': {
// placeholder: '请选择默认每页条数',
// options: [
// { label: '5', value: 5 },
// { label: '10', value: 10 },
// { label: '20', value: 20 },
// { label: '30', value: 30 },
// { label: '40', value: 40 },
// { label: '50', value: 50 },
// ]
// }
// }
// },
// handler: async (model, params, context) => {
// const { apiClient } = context.payload;
// const { collectionName, page = 1, pageSize = 10 } = params;
// const response = await apiClient.resource(collectionName).list({
// page,
// pageSize,
// });
// model.setProps({
// data: response?.data || {},
// page: response?.data?.meta?.page || 1,
// pageSize: response?.data?.meta?.pageSize || 10
// });
// },
// };
// // 转换为表格列
// const tableColumnsFilter: IFilter<BaseFlowModel> = {
// name: 'block:demo:columns',
// group: 'demo',
// title: '转换表格列',
// uiSchema: {},
// handler: (model, params, context) => {
// const fields = model.getProps().fields || [];
// // 将字段转换为表格列配置
// const columns = fields.filter(field => field.visible !== false).map(field => ({
// title: field,
// dataIndex: field,
// key: field,
// }));
// model.setProps('columns', columns);
// },
// };
// // 工具栏按钮选项配置过滤器
// const toolbarOptionsFilter: IFilter<BaseFlowModel> = {
// name: 'action:demo:toolbar:options',
// group: 'action',
// title: '工具栏按钮配置',
// uiSchema: {
// text: {
// type: 'string',
// title: '按钮文本',
// 'x-component': 'Input',
// },
// icon: {
// type: 'string',
// title: '图标',
// enum: ['RedoOutlined', 'EyeOutlined', 'PlusOutlined', 'DeleteOutlined'],
// 'x-component': 'Select',
// },
// buttonType: {
// type: 'string',
// title: '按钮类型',
// enum: ['primary', 'default', 'dashed', 'link', 'text'],
// 'x-component': 'Select',
// },
// size: {
// type: 'string',
// title: '按钮大小',
// enum: ['large', 'middle', 'small'],
// 'x-component': 'Select',
// },
// danger: {
// type: 'boolean',
// title: '危险按钮',
// 'x-component': 'Switch',
// }
// },
// handler: (model, params, context) => {
// model.setProps('buttonOptions', {
// text: params?.text || '按钮',
// icon: params?.icon || 'RedoOutlined',
// buttonType: params?.buttonType || 'default',
// size: params?.size || 'middle',
// danger: params?.danger || false
// });
// },
// };
// // 触发器配置过滤器
// const actionOnFilter: IFilter<BaseFlowModel> = {
// name: 'action:demo:on',
// group: 'action',
// title: '触发器配置',
// uiSchema: {
// on: {
// type: 'string',
// title: '触发方式',
// enum: ['onClick', 'onDoubleClick', 'onHover'],
// 'x-component': 'Select',
// }
// },
// handler: (model, params, context) => {
// model.setProps('on', params?.on || 'onClick');
// },
// };
// // 事件触发过滤器(触发指定事件)
// const eventTriggerFilter: IFilter<BaseFlowModel> = {
// name: 'action:demo:trigger',
// group: 'action',
// title: '事件触发',
// uiSchema: {
// event: {
// type: 'string',
// title: '要触发的事件',
// 'x-component': 'Input',
// }
// },
// handler: (model, params, context) => {
// // 从params中获取要触发的事件名称
// const eventName = context?.payload?.event;
// if (eventName) {
// model.setProps('triggerEvent', (payload) => {
// eventBus.dispatchEvent(eventName, payload);
// });
// }
// },
// };
// // 注册过滤器
// filterFlowManager.addFilter(linkagesFilter);
// filterFlowManager.addFilter(fieldsFilter);
// filterFlowManager.addFilter(actionsFilter);
// filterFlowManager.addFilter(dataFilter);
// filterFlowManager.addFilter(tableColumnsFilter);
// filterFlowManager.addFilter(toolbarOptionsFilter);
// filterFlowManager.addFilter(actionOnFilter);
// filterFlowManager.addFilter(eventTriggerFilter);
// // 注册过滤流程
// filterFlowManager.addFlow({
// key: 'block:demo:table',
// title: '表格区块filterflow',
// steps: [
// {
// key: 'block:common:linkages',
// filterName: 'block:common:linkages',
// title: '联动规则' // 配置规则,是否显示表格
// },
// {
// key: 'block:common:fields',
// filterName: 'block:common:fields',
// title: '字段配置', // 配置显示列
// },
// {
// key: 'block:common:data',
// filterName: 'block:common:data',
// title: '数据配置' // 数据加载
// },
// {
// key: 'block:demo:columns',
// filterName: 'block:demo:columns',
// title: '表格列配置' // 表格列转换,不放开配置
// },
// {
// key: 'block:demo:actions',
// filterName: 'block:demo:actions',
// title: '表格操作配置' // 表格操作转换,不放开配置
// }
// ]
// });
// // 注册工具栏按钮过滤流程
// filterFlowManager.addFlow({
// key: 'action:demo:toolbar',
// title: '工具栏按钮过滤流程',
// steps: [
// {
// key: 'action:demo:toolbar:options',
// filterName: 'action:demo:toolbar:options',
// title: '按钮配置'
// },
// {
// key: 'block:common:linkages',
// filterName: 'block:common:linkages',
// title: '联动规则'
// },
// {
// key: 'action:demo:on',
// filterName: 'action:demo:on',
// title: '触发方式配置'
// },
// {
// key: 'action:demo:trigger',
// filterName: 'action:demo:trigger',
// title: '事件触发'
// }
// ]
// });
// const updateStepConfig = function(type: 'event' | 'filter', flowName: string, stepKey: string, setConfigs?) {
// const flow = type === 'event' ? eventFlowManager.getFlow(flowName) : filterFlowManager.getFlow(flowName);
// const step = flow.getStep(stepKey);
// eventBus.dispatchEvent('block:configure:click', {
// payload: {
// step,
// currentParams: mockBlockConfigs.configData[`${type}Steps`][flowName][stepKey],
// onChange: (value) => {
// _.set(mockBlockConfigs.configData[`${type}Steps`][flowName], stepKey, value);
// setConfigs?.(mockBlockConfigs, true);
// }
// }
// });
// }
// const updateActionConfig = function() {
// // 显示
// }
// // 更改配置的按钮真实场景中用x-settings
// const ConfigureButtons = () => {
// const { setConfigs } = useBlockConfigs();
// return (
// <Space>
// <Button type="default" onClick={() => {
// updateStepConfig('filter', 'block:demo:table', 'block:common:fields', setConfigs);
// }}>配置显示列</Button>
// <Button type="default" onClick={() => {
// updateStepConfig('filter', 'block:demo:table', 'block:common:linkages', setConfigs);
// }}>联动规则</Button>
// <Button type="default" onClick={() => {
// updateStepConfig('filter', 'block:demo:table', 'block:common:data', setConfigs);
// }}>默认每页条数</Button>
// {/* <Button type="default" onClick={() => {
// // updateStepConfig('filter', 'block:demo:table', 'block:common:fields', setConfigs);
// updateActionConfig();
// }}>配置操作</Button> */}
// </Space>
// );
// };
// // 图标映射
// const IconComponents = {
// RedoOutlined,
// EyeOutlined,
// PlusOutlined,
// DeleteOutlined
// };
// interface ToolbarActionProps {
// event: string;
// filterSteps: Record<string, any>;
// eventSteps: Record<string, any>;
// }
// const ToolbarAction = (props: ToolbarActionProps) => {
// const compile = useCompile();
// const { event, filterSteps, eventSteps } = props;
// const filterContext: FilterHandlerContext = {
// meta: {
// params: {
// ...filterSteps
// }
// },
// payload: {
// event,
// compile,
// eventParams: eventSteps
// }
// };
// // 创建模型
// const [model] = useState(() => new BaseFlowModel('toolbar-action-model'));
// const { reApplyFilters } = useApplyFilters('action:demo:toolbar', model, filterContext);
// const {
// buttonOptions,
// on,
// triggerEvent,
// } = model.getProps();
// if (!buttonOptions) return null;
// const { text, icon, buttonType, size, danger } = buttonOptions;
// const IconComponent = icon ? IconComponents[icon] : null;
// const handleClick = () => {
// triggerEvent && triggerEvent({
// payload: {
// params: eventSteps
// }
// });
// };
// return (
// <Button
// type={buttonType as any}
// size={size as any}
// danger={danger}
// icon={IconComponent && <IconComponent />}
// onClick={on === 'onClick' ? handleClick : undefined}
// onDoubleClick={on === 'onDoubleClick' ? handleClick : undefined}
// onMouseEnter={on === 'onHover' ? handleClick : undefined}
// >
// {text}
// </Button>
// );
// };
// // 主表格组件
// const DemoTable: React.FC<{ configKey: string }> = ({ configKey }) => {
// const compile = useCompile();
// const filterContext = {
// payload: {
// configKey,
// apiClient: mockApiClient,
// compile,
// }
// };
// // 创建模型
// const [model] = useState(() => new BaseFlowModel('table-model'));
// const { reApplyFilters } = useApplyFilters('block:demo:table', model, filterContext);
// const {
// columns = [],
// data = { data: [], meta: { count: 0 } },
// actions = { toolbar: [], row: [] },
// page = 1,
// pageSize = 10,
// $break = false
// } = model.getProps();
// return (
// <>
// {
// $break ? null : (
// <>
// <Flex justify="flex-end" style={{ marginBottom: '8px' }}>
// <Space>
// {actions.toolbar.map((action: any) => (
// <ToolbarAction key={action.key} event={action.event} filterSteps={action.filterSteps} eventSteps={action.eventSteps} />
// ))}
// </Space>
// </Flex>
// <Table dataSource={data?.data} columns={columns} rowKey="id" pagination={{
// current: page,
// pageSize: pageSize,
// total: data?.meta?.count || 0,
// }} />
// </>
// )
// }
// </>
// );
// };
// // 主组件
// const EventFilterTableDemo2: React.FC = () => {
// return (
// <App>
// <FilterFlowProvider filterFlowManager={filterFlowManager}>
// <BlockConfigsProvider value={mockBlockConfigs}>
// <SchemaComponentProvider>
// <ConfigureButtons />
// <SchemaComponent
// schema={{
// type: 'void',
// name: 'demoTable',
// 'x-component': 'div',
// properties: {
// table: {
// type: 'void',
// 'x-component': 'DemoTable',
// 'x-component-props': {
// configKey: mockBlockConfigs.key
// }
// }
// }
// }}
// components={{
// DemoTable
// }}
// />
// </SchemaComponentProvider>
// </BlockConfigsProvider>
// </FilterFlowProvider>
// </App>
// );
// };
// export default EventFilterTableDemo2;

View File

@ -0,0 +1,198 @@
[
{
"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
}
]
}
]

View File

@ -0,0 +1,98 @@
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;
}

View File

@ -0,0 +1,352 @@
import React from 'react';
import { Table, Button, Space, Pagination, Spin, Divider, ButtonProps } from 'antd';
import { Application, Plugin } from '@nocobase/client';
import {
BlockModel,
FlowsDropdownButton,
FlowsContextMenu,
AddAction,
FlowResource,
FlowContext,
FlowModel,
useFlowModel,
withFlowModel,
FlowsFloatContextMenu,
} from '@nocobase/flow-engine';
import { observer } from '@formily/react';
const Demo = () => {
const uid = 'table-block';
const model = useFlowModel(uid, 'DemoTableBlockModel') as any;
return (
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
<AddAction model={model} />
<ActionsComponent model={model} />
<Divider />
<FlowsDropdownButton model={model} text="表格配置" size="small" type="dashed" />
<TableBlock model={model} />
</div>
);
};
// 表格组件
const TableComponent = ({
loading = false,
columns = [],
dataSource = [],
pagination = { current: 1, pageSize: 10, total: 0 },
height = 400,
title = '数据表格',
onPaginationChange,
}) => {
return (
<div style={{ marginTop: 16, height: height }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
padding: '12px 16px',
background: '#fff',
borderRadius: 6,
border: '1px solid #d9d9d9',
}}
>
<h3 style={{ margin: 0 }}>{title}</h3>
</div>
<Spin spinning={loading}>
<Table dataSource={dataSource} columns={columns} pagination={false} scroll={{ y: height }} rowKey="id" />
</Spin>
{pagination.total > 0 && (
<div style={{ marginTop: 16, textAlign: 'right' }}>
<Pagination
current={pagination.current}
pageSize={pagination.pageSize}
total={pagination.total}
showSizeChanger
showQuickJumper
showTotal={(total, range) => `显示 ${range[0]}-${range[1]} 条,共 ${total} 条数据`}
onChange={onPaginationChange}
onShowSizeChange={onPaginationChange}
/>
</div>
)}
</div>
);
};
const ActionButton = withFlowModel(
(props: ButtonProps & { text?: string }) => {
const { text, ...rest } = props;
return <Button {...rest}>{text}</Button>;
},
{
settings: {
component: FlowsFloatContextMenu,
// component: FlowsContextMenu
},
},
);
// Actions组件 - 渲染工具栏操作
const ActionsComponent = observer(({ model }: { model: BlockModel }) => {
if (!model.actions.size) return null;
return (
<div style={{ marginBottom: 16, textAlign: 'right' }}>
<Space>
{Array.from(model.actions.values()).map((action) => (
<ActionButton key={action.uid} model={action} />
))}
</Space>
</div>
);
});
const TableBlock = withFlowModel(TableComponent);
// 创建继承自BlockModel的DemoTableBlockModel
class DemoTableBlockModel extends BlockModel {
private resources: Map<string, any> = new Map();
setResource(key: string, resource: any): void {
this.resources.set(key, resource);
}
getResource(key: string): any {
return this.resources.get(key);
}
static {
this.registerFlow({
key: 'setProps',
title: '表格属性',
auto: true,
steps: {
setFields: {
use: 'setTableFields',
title: '字段配置',
defaultParams: {
fields: ['id', 'name', 'age', 'email', 'city'],
},
},
convertToColumns: {
handler: async (ctx: FlowContext) => {
// 将字段转换为表格列
const props = ctx.model.getProps();
const fields = props['fields'] || [];
const fieldLabels = {
id: 'ID',
name: '姓名',
age: '年龄',
email: '邮箱',
city: '城市',
};
const columns = fields.map((field) => ({
title: fieldLabels[field] || field,
dataIndex: field,
key: field,
width: field === 'id' ? 80 : field === 'email' ? 200 : 120,
}));
ctx.model.setProps('columns', columns);
},
},
setTitle: {
use: 'setTableTitle',
title: '设置标题',
defaultParams: { title: '用户数据表格' },
},
setupDataSource: {
handler: async (ctx: FlowContext) => {
// 设置数据源
const dataResource = (ctx.model as any).getResource('data');
const tableData = dataResource?.getData() || [];
ctx.model.setProps('dataSource', tableData);
},
},
setupPaginationHandler: {
handler: async (ctx: FlowContext) => {
// 设置分页处理函数
const onPaginationChange = (page: number, pageSize: number) => {
ctx.model.dispatchEvent('table:pagination:change', { current: page, pageSize });
};
ctx.model.setProps('onPaginationChange', onPaginationChange);
},
},
initDataResource: {
handler: async (ctx: FlowContext) => {
const dataResource = new FlowResource();
(ctx.model as any).setResource('data', dataResource);
},
},
},
});
this.registerFlow({
key: 'pagination',
title: '分页操作',
on: {
eventName: 'table:pagination:change',
},
steps: {
updatePagination: {
handler: async (ctx: FlowContext, params) => {
const { current, pageSize } = params || {};
const currentPagination = ctx.model.getProps().pagination || {};
ctx.model.setProps('pagination', {
...currentPagination,
current: current || currentPagination.current,
pageSize: pageSize || currentPagination.pageSize,
});
ctx.model.applyFlow('loadData');
},
},
},
});
this.registerFlow({
key: 'loadData',
title: '数据加载',
steps: {
setLoading: {
handler: async (ctx: FlowContext) => {
ctx.model.setProps('loading', true);
},
},
fetchData: {
handler: async (ctx: FlowContext) => {
// 添加延迟以模拟真实API请求
await new Promise((resolve) => setTimeout(resolve, 500));
const props = ctx.model.getProps();
const pagination = props.pagination || { current: 1, pageSize: 10 };
try {
const mockData = generateMockData(pagination.current, pagination.pageSize);
const dataResource = (ctx.model as any).getResource('data');
if (dataResource) {
dataResource.setData(mockData.data);
}
ctx.model.setProps('pagination', {
...pagination,
total: mockData.total,
});
} catch (error) {
console.error('Failed to load data:', error);
}
},
},
updateDataSource: {
handler: async (ctx: FlowContext) => {
const dataResource = ctx.model['getResource']('data');
const tableData = dataResource?.getData() || [];
ctx.model.setProps('dataSource', tableData);
},
},
setLoadingEnd: {
handler: async (ctx: FlowContext) => {
ctx.model.setProps('loading', false);
},
},
},
});
}
}
function generateMockData(page: number, pageSize: number) {
const total = 256;
const startIndex = (page - 1) * pageSize;
const data = [];
for (let i = 0; i < pageSize && startIndex + i < total; i++) {
const id = startIndex + i + 1;
data.push({
id,
name: `用户${id}`,
age: 20 + (id % 40),
email: `user${id}@example.com`,
city: ['北京', '上海', '广州', '深圳', '杭州', '南京', '成都', '武汉'][id % 8],
});
}
return { data, total };
}
class DemoTablePlugin extends Plugin {
async load() {
this.app.flowEngine.registerModels({ DemoTableBlockModel });
this.app.flowEngine.registerAction({
name: 'setTableFields',
title: '字段配置',
uiSchema: {
fields: {
type: 'array',
title: '显示列',
'x-component': 'Select',
'x-component-props': {
mode: 'multiple',
options: [
{ label: 'ID', value: 'id' },
{ label: '姓名', value: 'name' },
{ label: '年龄', value: 'age' },
{ label: '邮箱', value: 'email' },
{ label: '城市', value: 'city' },
],
},
},
},
handler: (ctx: FlowContext, params: any) => {
if (params?.fields) {
ctx.model.setProps('fields', params.fields);
}
},
});
this.app.flowEngine.registerAction({
name: 'setTableTitle',
title: '标题设置',
uiSchema: {
title: {
type: 'string',
title: '表格标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
handler: (ctx: FlowContext, params: any) => {
if (params?.title != null) {
ctx.model.setProps('title', params.title);
}
},
});
this.app.flowEngine.registerAction({
name: 'loadTableData',
title: '加载数据',
uiSchema: {},
handler: (ctx: FlowContext, params: any) => {
ctx.model.applyFlow('loadData');
},
});
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [DemoTablePlugin],
});
export default app.getRootComponent();

View File

@ -0,0 +1,9 @@
# Model
## Table Block
这个示例展示了一个完整的表格组件,包含数据加载、分页、字段配置等功能。
**使用说明**: 右键点击下方的表格区域,可以打开配置菜单设置显示字段、表格标题等参数。
<code src="./demos/models/table.tsx"></code>

View File

@ -0,0 +1 @@
# Flow Actions

View File

@ -0,0 +1,92 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, IFlowModelRepository } from '@nocobase/flow-engine';
import React from 'react';
// 实现一个本地存储的模型仓库,负责模型的持久化
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
// 从本地存储加载模型数据
async load(uid: string) {
const data = localStorage.getItem(`flow-model:${uid}`);
if (!data) return null;
return JSON.parse(data);
}
// 将模型数据保存到本地存储
async save(model: FlowModel) {
localStorage.setItem(`flow-model:${model.uid}`, JSON.stringify(model.serialize()));
console.log('Saving model:', model);
return model;
}
// 从本地存储中删除模型数据
async destroy(uid: string) {
localStorage.removeItem(`flow-model:${uid}`);
return true;
}
}
// 自定义模型类,继承自 FlowModel
class MyModel extends FlowModel {
// 渲染模型内容
render() {
return <div>{this.props.name}</div>;
}
}
// 为 MyModel 配置流
MyModel.registerFlow({
key: 'defaultFlow',
title: 'Default Flow',
auto: true,
steps: {
step1: {
uiSchema: {
name: {
type: 'string',
title: 'Name',
'x-component': Input,
},
},
// 步骤处理函数,设置模型属性
handler(ctx, params) {
ctx.model.setProps('name', params.name);
},
},
},
});
// 插件类,负责注册模型、仓库,并加载或创建模型实例
class PluginHelloModel extends Plugin {
async load() {
// 注册自定义模型
this.flowEngine.registerModels({ MyModel });
// 设置模型仓库(本地存储实现)
this.flowEngine.setModelRepository(new FlowModelRepository());
// 加载或创建模型实例(如不存在则创建并初始化)
const model = await this.flowEngine.loadOrCreateModel({
uid: 'my-model',
use: 'MyModel',
stepParams: {
defaultFlow: {
step1: {
name: 'NocoBase',
},
},
},
});
// 注册路由,渲染模型
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} showFlowSettings />,
});
}
}
// 创建应用实例,注册插件
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,123 @@
# FlowAction
`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作Action的核心对象。每个操作Action封装一段可执行的业务逻辑可以在多个流步骤中复用支持参数配置、UI 配置和类型推断。
---
## ActionDefinition 接口
```ts
interface ActionDefinition {
name: string; // 操作唯一标识,必须唯一
title?: string; // 操作显示名称(可选)
uiSchema?: Record<string, ISchema>; // (可选)用于参数配置界面渲染
defaultParams?: Record<string, any>; // (可选)默认参数
paramsRequired?: boolean; // 可选是否需要参数配置为true时添加模型前会打开配置对话框
hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏该步骤
handler: (ctx: FlowContext, params: any) => Promise<any> | any; // 操作执行逻辑
}
```
---
## 定义操作的方式
### 1. 使用 defineAction 工具函数
推荐方式,结构清晰、类型推断友好:
```ts
const myAction = defineAction({
name: 'actionName',
title: '操作显示名称',
uiSchema: {},
defaultParams: {},
paramsRequired: true, // 添加模型前强制打开配置对话框
hideInSettings: false, // 在设置菜单中显示
async handler(ctx, params) {
// 操作逻辑
},
});
```
### 2. 实现 ActionDefinition 接口
适合需要扩展属性或方法的场景:
```ts
class MyAction implements ActionDefinition {
name = 'actionName';
title = '操作显示名称';
uiSchema = {};
defaultParams = {};
paramsRequired = true; // 添加模型前强制打开配置对话框
hideInSettings = false; // 在设置菜单中显示
async handler(ctx, params) {
// 操作逻辑
}
}
```
---
## 注册操作
注册后可在流步骤中通过 `use` 字段复用:
```ts
flowEngine.registerAction({
name: 'actionName',
title: '操作显示名称',
uiSchema: {},
defaultParams: {},
paramsRequired: true, // 添加模型前强制打开配置对话框
hideInSettings: false, // 在设置菜单中显示
handler(ctx, params) {
// 操作逻辑
},
});
flowEngine.registerAction(myAction); // 注册 defineAction 返回的对象
flowEngine.registerAction(new MyAction()); // 注册类实例
```
---
## 在流中复用操作
在流步骤定义中通过 `use` 字段引用已注册的操作:
```ts
steps: {
step1: {
use: 'actionName', // 复用已注册的操作
defaultParams: {},
paramsRequired: true, // 可以在步骤级别覆盖操作的paramsRequired设置
hideInSettings: false, // 可以在步骤级别覆盖操作的hideInSettings设置
},
}
```
---
## 配置选项说明
### paramsRequired
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 当设置为 `true` 时,在添加该步骤模型前会强制打开参数配置对话框,确保用户配置必要的参数。适用于需要用户必须配置参数才能正常工作的操作。
### hideInSettings
- **类型**: `boolean`
- **默认值**: `false`
- **说明**: 当设置为 `true` 时,该步骤将在设置菜单中隐藏,用户无法通过 Settings 界面直接添加该步骤。适用于初始化配置场景。
---
## 总结
- **FlowAction** 让流步骤逻辑高度复用,便于维护和扩展。
- 支持多种定义方式,适应不同复杂度的业务场景。
- 可通过 `uiSchema``defaultParams` 配置参数界面和默认值,提升易用性。

View File

@ -0,0 +1,242 @@
# FlowContext
`FlowContext` 是流引擎在执行流步骤时传递的上下文对象。它用于在步骤处理函数中控制流的执行、传递数据、记录日志等。通过 `ctx`,可以灵活地影响流的走向和行为,实现复杂的业务流编排。
---
## 主要属性与方法
| 属性/方法 | 说明 |
|------------------|-------------------------------------------------------------------------------------|
| `ctx.exit()` | 立即终止整个流的执行,后续步骤不再执行。适用于遇到致命错误或业务条件不满足时主动中断流。| |
| `ctx.logger` | 日志记录工具,支持 `info``warn``error``debug` 等方法。用于输出调试信息和业务日志。|
| `ctx.stepResults`| 存储每个步骤的返回结果,结构为 `{ 步骤名: 返回值 }`,便于后续步骤访问前置结果。 |
| `ctx.shared` | 流上下文中的共享数据对象,可用于步骤间数据传递,可读可写。适合存放流内需要多步骤共享的变量。|
| `ctx.model` | 当前流关联的数据模型实例,通常用于在流步骤中访问和操作业务数据。|
| `ctx.globals` | 系统初始化时设置的全局上下文,跨流共享,只读。适合存放全局配置、常量等。|
| `ctx.extra` | 额外上下文对象,通过 `applyFlow` 传入,仅在本次流执行时有效,适合传递临时数据,只读。 |
| `ctx.app` | 当前应用实例,可用于访问应用级别的服务和资源。|
---
## 四类变量的定义与访问
流上下文变量分为四类,分别对应不同的定义方式和访问范围:
### 1. 全局变量ctx.globals
- **定义方式**:在流引擎初始化时通过 `flowEngine.defineGlobalVars()` 声明类型。
- **适用场景**:全局配置、当前用户、系统常量等,所有流和步骤可访问,只读。
- **访问方式**`ctx.globals.xxx`
```ts
flowEngine.defineGlobalVars({
user: { type: 'object', label: '当前用户' },
roles: { type: 'array', label: '当前角色' },
systemDate: { type: 'date', label: '系统日期' },
});
// 在流步骤中访问
const user = ctx.globals.user;
```
### 2. 局部变量ctx.extra
- **定义方式**:在模型层通过 `MyModel.defineExtraVars()` 声明类型,流执行时通过 `applyFlow` 传入。
- **适用场景**:当前数据、选中记录等与本次流强相关的临时数据,只读。
- **访问方式**`ctx.extra.xxx`
```ts
MyModel.defineExtraVars({
currentTable: { type: 'string', label: '当前数据表' },
currentRecord: { type: 'object', label: '当前数据' },
selectedRecords: { type: 'array', label: '选中记录' },
});
// 传入流
const extraContext = {
currentTable: 'users',
currentRecord: { id: 1, name: '张三' },
selectedRecords: [{ id: 2 }, { id: 3 }],
};
await model.applyFlow('myFlow', extraContext);
// 在流步骤中访问
const record = ctx.extra.currentRecord;
```
### 3. 流共享变量ctx.shared
- **定义方式**:在流定义时通过 `defineFlow({ shared })` 声明类型。
- **适用场景**:流内多步骤共享的数据,可读可写。
- **访问方式**`ctx.shared.xxx`
```ts
const myFlow = defineFlow({
key: 'myFlow',
shared: {
flowParam: { type: 'string', label: '流参数' },
},
steps: { ... }
});
// 在流步骤中访问和修改
ctx.shared.flowParam = 'newValue';
```
### 4. 步骤输出变量ctx.stepResults
- **定义方式**:每个步骤通过 `output` 字段声明类型handler 返回值自动存储。
- **适用场景**:步骤间结果传递,后续步骤可访问前置步骤的输出,只读。
- **访问方式**`ctx.stepResults.步骤名`
```ts
step1: {
output: { type: 'string', label: '问候语' },
async handler(ctx, params) {
return 'hello';
}
},
step2: {
async handler(ctx, params) {
const prev = ctx.stepResults.step1;
return prev + ' world';
}
}
```
---
## 常见场景示例
### 1. 终止流
当遇到不可恢复的错误或业务条件不满足时,可调用 `ctx.exit()` 立即终止流。
```ts
async handler(ctx, params) {
if (params.shouldExit) {
ctx.exit();
}
// ...其他逻辑...
}
```
### 2. 访问前置步骤结果
通过 `ctx.stepResults` 可以方便地获取前置步骤的返回值,实现步骤间的数据依赖。
```ts
// step1 返回一个结果
async handler(ctx, params) {
return 'hello';
}
// step2 访问 step1 的返回值
async handler(ctx, params) {
const prev = ctx.stepResults.step1;
// 基于前置步骤结果处理
return prev + ' world';
}
```
### 3. 上下文的传递
流上下文的传递分为全局和局部两种场景:
- **全局上下文**:适用于多个流共享的数据(如全局配置、服务实例等),建议存放在 `this.flowEngine.context` 中,由流引擎统一管理。
- **局部上下文**:适用于某次流执行时临时传递的数据(如用户输入、请求参数等),通过 `applyFlow``extraContext` 参数传入,仅在本次流执行期间有效。
示例代码如下:
```ts
class FlowModel {
async applyFlow(flowKey, extraContext) {
const flowContext = new FlowContext();
// 绑定当前模型实例
flowContext.set('model', this);
// 注入全局上下文(只读)
flowContext.set('globals', this.flowEngine.context);
// 注入本次流的额外上下文(只读)
flowContext.set('extra', extraContext);
// 执行流中间件
await compose(this.engine.middlewares)(flowContext);
}
}
```
> **注意事项:**
> - `globals``extra` 都为只读属性,建议仅用于读取,不要在流中修改。
> - `shared` 可读可写,适合在流内多步骤间传递和修改数据。
> - 合理区分全局与局部上下文,避免数据污染和副作用。
### 4. 日志记录示例
日志记录有助于流调试和问题追踪。推荐在关键步骤、异常处理等场景下使用。
```ts
ctx.logger.info('步骤开始', { step: 'step1', params });
ctx.logger.error('发生错误', error);
```
---
## 完整流示例
以下为一个完整的流定义与调用示例,涵盖了流注册、步骤处理、上下文传递等关键环节:
```ts
class MyModel extends FlowModel {}
const myFlow = defineFlow({
key: 'myFlow',
shared: {
flowParam: { type: 'string', label: '流参数' },
},
steps: {
step1: {
output: { type: 'string', label: '问候语' },
async handler(ctx, params) {
if (params.shouldExit) {
ctx.exit();
}
return 'hello';
},
},
step2: {
async handler(ctx, params) {
if (params.shouldSkip) {
return;
}
const prev = ctx.stepResults.step1;
return prev + ' world';
},
}
},
});
MyModel.registerFlow(myFlow);
const model = new MyModel({
stepParams: {
myFlow: {
step1: { shouldExit: false },
step2: { shouldSkip: true },
}
},
});
const extraContext = { userId: 123, requestId: 'abc-xyz' };
await model.applyFlow('myFlow', extraContext);
```
---
## 最佳实践建议
- 合理利用 `ctx.shared` 进行步骤间数据传递,避免滥用全局上下文。
- 日志记录应包含关键信息,便于后续排查和分析。
- 对于只读上下文(如 `globals``extra`),避免在流中修改,确保数据一致性。
- 流步骤应尽量保持单一职责,便于维护和复用。
如需进一步扩展 `FlowContext`,可根据实际业务需求自定义属性和方法,但建议遵循上下文只读/可写的设计原则,确保流的可控性和可维护性。

View File

@ -0,0 +1,185 @@
# FlowDefinition
`FlowDefinition` 是 NocoBase 流引擎中用于描述和注册流Flow的核心定义对象。它用于定义流的唯一标识`key`)、各个步骤(`steps`)、事件触发条件、参数配置及处理逻辑,是实现流自动化和业务编排的基础。
---
## 核心结构
```ts
interface FlowDefinition {
key: string; // 流唯一标识
on?: { event: string }; // 可选:事件触发配置
auto?: boolean; // 可选:是否自动运行
steps: Record<string, StepDefinition>; // 流步骤定义
}
interface StepDefinition {
use?: string; // 可选:引用已注册的全局 Action
defaultParams?: any; // 默认参数
uiSchema?: any; // 可选:用于 FlowSettings 配置界面
handler?: (ctx: any, params: any) => Promise<any>; // 可选:步骤处理函数
}
```
---
## 定义流方式
### 函数式定义(适合简单流)
结构清晰、类型推断友好,推荐用于大多数场景。
```ts
type MyFlowSteps = {
step1: { name: string };
step2: { age: number };
};
const myFlow = defineFlow<MyFlowSteps>({
key: 'myFlow',
on: { event: 'user.created' }, // 监听 user.created 事件自动触发
steps: {
step1: {
defaultParams: {},
async handler(ctx, params) {
// 步骤 1 的处理逻辑
// 例如console.log(params.name);
}
},
step2: {
uiSchema: {}, // 可用于 UI 配置
defaultParams: {},
async handler(ctx, params) {
// 步骤 2 的处理逻辑
// 例如console.log(params.age);
}
},
},
});
MyFlowModel.registerFlow(myFlow); // 注册流
```
---
### 类式定义(适合复杂流)
当流较复杂、需要继承或拆分逻辑时,推荐使用类定义方式。
```ts
class MyFlowDefinition implements FlowDefinition {
key = 'MyFlowDefinition';
steps = {
step1: {
use: 'globalAction', // 复用全局已注册 Action
defaultParams: {},
},
step2: {
defaultParams: {},
async handler(ctx, params) {
// 步骤 2 的处理逻辑
}
},
step3: {
uiSchema: {},
defaultParams: {},
async handler(ctx, params) {
// 步骤 3 的处理逻辑
}
},
};
}
MyFlowModel.registerFlow(new MyFlowDefinition());
```
如需扩展流执行上下文、步骤参数结构、UI 配置展示等,可以进一步封装自定义工具函数或继承 `FlowDefinition`
```ts
// 示例:扩展 FlowDefinition 和 FlowModel
// 自定义流定义,继承 FlowDefinition
class TableColumnFlowDefinition implements FlowDefinition {
baseSteps = {};
// 可以在此扩展更多自定义属性或方法
}
// 自定义模型,继承 FlowModel
class TableColumnFlowModel extends FlowModel {}
// 注册流定义到模型
TableColumnFlowModel.registerFlow(new TableColumnFlowDefinition());
// 进一步继承模型,实现更细粒度的定制
class SelectTableColumnFlowModel extends TableColumnFlowModel {}
// 注册带有额外步骤的流定义
SelectTableColumnFlowModel.registerFlow(
new TableColumnFlowDefinition({
otherSteps: {}
// 可以传递更多自定义步骤
})
);
```
---
## 模型中的流执行
一个模型(`FlowModel`)可以注册多个不同的流(`FlowDefinition`)。在执行流时,可以通过 `stepParams` 为每个步骤传参,实现灵活定制。
### 1. 配置步骤参数
在模型实例化时配置各步骤的参数,或使用 `model.setStepParams(...)` 动态设置:
```ts
type MyFlows = {
myFlow: MyFlowSteps;
};
const myModel = new MyFlowModel<MyFlows>({
stepParams: {
myFlow: {
step1: { name: 'Tao Tao' }, // 给 step1 传递参数
step2: { age: 6 }, // 给 step2 传递参数
},
},
});
// 动态设置步骤参数(可选)
myModel.setStepParams('myFlow', 'step1', { name: '小明' });
```
### 2. 执行流
```ts
await myModel.applyFlow('myFlow'); // 主动执行指定流
myModel.dispatchEvent('user.created'); // 分发事件触发流(如流配置了 on.event
await myModel.applyAutoFlows(); // 执行所有 auto=true 的流
```
---
## 速查表
### FlowDefinition 配置速查表
| 字段 | 类型 | 说明 |
| ----------- | -------------------------------- | ---------------------------------- |
| `key` | `string` | 流唯一标识,必须配置 |
| `on` | `{ event: string }` | (可选)事件触发配置 |
| `auto` | `boolean` | (可选)是否在 `applyAutoFlows()` 中自动执行流 |
| `steps` | `Record<string, StepDefinition>` | 步骤集合,键为步骤名,值为步骤定义 |
### StepDefinition 配置速查表
| 字段 | 类型 | 说明 |
| --------------- | -------------------------------------- | ------------------------------------- |
| `use` | `string` | (可选)引用已注册的全局 Action |
| `defaultParams` | `any` | 步骤的默认参数 |
| `uiSchema` | `any` | (可选)用于 FlowSettings UI 渲染 |
| `handler` | `(ctx, params) => Promise<any>` | (可选)步骤执行逻辑,若未定义则使用 `use` 指定的全局 Action |
---

View File

@ -0,0 +1,89 @@
# FlowEngine
`FlowEngine` 是 NocoBase 前端流引擎的核心调度与管理类专为前端流自动化与业务逻辑编排设计。它负责流模型Model与操作Action的注册、生命周期管理、持久化、远程同步等为前端场景下的流运行和扩展提供统一的环境与机制。
---
## 主要方法与属性
### Model 类注册与管理
- **registerModels(models: Record<string, ModelConstructor>): void**
批量注册模型类。
- **getModelClass(name: string): ModelConstructor | undefined**
获取已注册的模型类。
- **getModelClasses(): Map<string, ModelConstructor>**
获取所有已注册的模型类(构造函数)。返回一个 Mapkey 为模型名称value 为对应的模型类。常用于遍历、动态生成模型列表或批量操作等场景。
---
### Model 实例管理
- **createModel\<T extends FlowModel = FlowModel\>(options: CreateModelOptions): T**
创建并注册一个模型实例。如果指定 UID 已存在,则返回现有实例。支持泛型以确保正确的模型类型推导。
- **getModel\<T extends FlowModel = FlowModel\>(uid: string): T | undefined**
根据 UID 获取本地模型实例。
- **removeModel(uid: string): boolean**
销毁并移除一个本地模型实例。
---
### Model 持久化与远程操作
- **setModelRepository(modelRepository: IFlowModelRepository): void**
注入模型仓库(通常用于远程数据源/持久化适配器)。
- **async loadModel\<T extends FlowModel = FlowModel\>(uid: string): Promise\<T | null\>**
从远程仓库加载模型数据,并创建本地实例。如果模型不存在则返回 null。
- **async loadOrCreateModel\<T extends FlowModel = FlowModel\>(options: CreateModelOptions): Promise\<T | null\>**
从远程仓库加载模型,如果不存在则创建新模型实例并持久化。如果仓库未设置则返回 null。
- **async saveModel(model: FlowModel): Promise<any>**
保存模型到远程仓库。
- **async destroyModel(uid: string): Promise<boolean>**
从远程仓库删除模型,并移除本地实例。
---
### Action 注册与获取
- **registerAction\<TModel extends FlowModel = FlowModel\>(nameOrDefinition, options?): void**
注册一个 Action可传入名称和选项或完整的 ActionDefinition 对象。支持泛型以确保正确的模型类型推导。Action 是流中的可复用操作单元。
- **getAction\<TModel extends FlowModel = FlowModel\>(name: string): ActionDefinition\<TModel\> | undefined**
获取已注册的 Action 定义。支持泛型以确保正确的模型类型推导。
---
### Flow 注册
- **registerFlow\<TModel extends FlowModel = FlowModel\>(modelClassName: string, flowDefinition: FlowDefinition\<TModel\>): void**
注册一个 Flow 到指定的模型类。
---
### FlowSettings 管理
- **flowSettings.registerComponents(components): void**
添加组件到 flowSettings 的组件注册表中, 这些组件可以在 flow step 的 uiSchema 中使用。
- **flowSettings.registerScopes(scopes): void**
添加作用域到 flowSettings 的作用域注册表中, 这些作用域可以在 flow step 的 uiSchema 中使用。
- **flowSettings.load(): Promise\<void\>**
加载 FlowSettings 相关资源,未启用 FlowSettings 时可不调用。
- **flowSettings.openStepSettingsDialog(props: StepSettingsDialogProps)**
显示单个步骤的配置界面。
---
## 示例
<code src="./demos/quickstart.tsx"></code>

View File

@ -0,0 +1,2 @@
# FlowHooks

View File

@ -0,0 +1,146 @@
# FlowModelRenderer
`FlowModelRenderer` 是 NocoBase 流引擎中用于渲染和交互单个模型FlowModel的基础组件。它负责展示模型的主要内容并可通过不同方式集成流设置FlowModelSettings实现流的快捷管理与配置。
## Props 说明
```ts
interface FlowModelRendererProps {
model?: FlowModel;
uid?: string;
/** 是否显示流设置入口(如按钮、菜单等) */
showFlowSettings?: boolean; // 默认 false
/** 流设置的交互风格 */
flowSettingsVariant?: 'dropdown' | 'contextMenu' | 'modal' | 'drawer'; // 默认 'dropdown'
/** 是否在设置中隐藏移除按钮 */
hideRemoveInSettings?: boolean; // 默认 false
/** 是否跳过自动应用流,默认 false */
skipApplyAutoFlows?: boolean; // 默认 false
/** 当 skipApplyAutoFlows !== false 时,传递给 useApplyAutoFlows 的额外上下文 */
extraContext?: Record<string, any>
/** 是否为每个组件独立执行 auto flow默认 false */
independentAutoFlowExecution?: boolean; // 默认 false
}
```
### Props 详细说明
- **model**: 要渲染的 FlowModel 实例
- **uid**: 流模型的唯一标识符
- **showFlowSettings**: 是否显示流设置入口,如按钮、菜单等
- **flowSettingsVariant**: 流设置的交互风格
- `dropdown`: 下拉菜单形式(默认)
- `contextMenu`: 右键上下文菜单
- `modal`: 模态框形式(待实现)
- `drawer`: 抽屉形式(待实现)
- **hideRemoveInSettings**: 是否在设置中隐藏移除按钮,当设为 `true` 时,流设置菜单中不会显示删除/移除选项
- **skipApplyAutoFlows**: 是否跳过自动应用流。当设为 `true` 时,组件不会调用 `useApplyAutoFlows` hook
- **extraContext**: 额外的上下文数据,当 `skipApplyAutoFlows``false` 时传递给 `useApplyAutoFlows` hook
## 主要示例
```tsx | pure
// 基础示例
<FlowModelRenderer model={model} />
// 显示 flow settings
<FlowModelRenderer
model={model}
showFlowSettings
flowSettingsVariant={'dropdown'}
/>
// 显示设置但隐藏移除按钮
<FlowModelRenderer
model={model}
showFlowSettings={true}
hideRemoveInSettings={true}
/>
// 跳过自动应用流
<FlowModelRenderer
model={model}
skipApplyAutoFlows={true}
/>
// 传递额外上下文
<FlowModelRenderer
model={model}
extraContext={{ customData: 'value' }}
/>
// 完整配置示例
<FlowModelRenderer
model={model}
uid="unique-flow-id"
showFlowSettings={true}
flowSettingsVariant="contextMenu"
hideRemoveInSettings={false}
skipApplyAutoFlows={false}
extraContext={{
userId: 123,
permissions: ['read', 'write']
}}
/>
```
## 使用场景
### 1. 基础渲染
当只需要渲染流模型内容时,使用最简配置:
```tsx | pure
<FlowModelRenderer model={flowModel} />
```
### 2. 带设置功能的渲染
当需要用户能够配置流时,启用流设置:
```tsx | pure
<FlowModelRenderer
model={flowModel}
showFlowSettings={true}
flowSettingsVariant="dropdown"
/>
```
### 3. 带设置但禁用删除功能
当需要流设置但不允许用户删除时:
```tsx | pure
<FlowModelRenderer
model={flowModel}
showFlowSettings={true}
hideRemoveInSettings={true}
/>
```
### 4. 自定义流控制
当需要手动控制流应用时,可以跳过自动流:
```tsx | pure
<FlowModelRenderer
model={flowModel}
skipApplyAutoFlows={true}
/>
```
### 5. 传递自定义上下文
当需要向流传递特定上下文数据时:
```tsx | pure
<FlowModelRenderer
model={flowModel}
extraContext={{
currentUser: user,
formData: formValues,
permissions: userPermissions
}}
/>
```

View File

@ -0,0 +1,72 @@
# IFlowModelRepository
`IFlowModelRepository` 是 FlowEngine 的模型持久化接口定义了模型的远程加载、保存和删除等操作。通过实现该接口可以将模型的数据持久化到后端数据库、API 或其他存储介质,实现前后端的数据同步。
## 主要方法
- **load(uid: string): Promise<FlowModel \| null>**
根据唯一标识符 uid 从远程加载模型数据。
- **save(model: FlowModel): Promise<any>**
将模型数据保存到远程存储。
- **destroy(uid: string): Promise<boolean>**
根据 uid 从远程存储删除模型。
## FlowModelRepository 示例
```ts
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
constructor(private app: Application) {}
async load(uid: string) {
// 实现:根据 uid 获取模型
return null;
}
async save(model: FlowModel) {
console.log('Saving model:', model);
// 实现:保存模型
return model;
}
async destroy(uid: string) {
// 实现:根据 uid 删除模型
return true;
}
}
```
## 设置 FlowModelRepository
```ts
flowEngine.setModelRepository(new FlowModelRepository(this.app));
```
## FlowEngine 提供的模型管理方法
### 本地方法
```ts
flowEngine.createModel(options); // 创建本地模型实例
flowEngine.getModel(uid); // 获取本地模型实例
flowEngine.removeModel(uid); // 移除本地模型实例
```
### 远程方法(由 ModelRepository 实现)
```ts
await flowEngine.loadModel(uid); // 从远程加载模型
await flowEngine.saveModel(model); // 保存模型到远程
await flowEngine.destroyModel(uid); // 从远程删除模型
```
## model 实例方法
```ts
const model = this.flowEngine.createModel({
use: 'FlowModel',
});
await model.save(); // 保存到远程
await model.destroy(); // 从远程删除
```

View File

@ -0,0 +1,41 @@
# FlowModelSettings
`FlowModelSettings` 是用于管理和配置 FlowModel 上所有流Flow的专用组件。它为用户提供了多种交互入口方便在不同场景下快捷地查看、编辑和管理模型的流设置。
---
## 常见用法
除了和 FlowModelRenderer 集成,也可以单独使用,例如:
### 悬浮菜单
```tsx | pure
<FlowModelSettingsDropdown model={model}>
<a>Click me</a>
</FlowModelSettingsDropdown>
```
### 右键菜单
```tsx | pure
<FlowModelSettingsContextMenu model={model}>
<a>Click me</a>
</FlowModelSettingsContextMenu>
```
### 对话框
```tsx | pure
<FlowModelSettingsModal model={model}>
<a>Click me</a>
</FlowModelSettingsModal>
```
### 抽屉
```tsx | pure
<FlowModelSettingsDrawer model={model}>
<a>Click me</a>
</FlowModelSettingsDrawer>
```

View File

@ -0,0 +1,200 @@
# FlowModel
`FlowModel` 是 NocoBase 流引擎的基础模型类,支持流注册、流执行、属性管理、子模型管理、持久化等功能。所有业务模型均可继承自 FlowModel。
## 泛型支持
FlowModel 支持泛型,可以通过类型参数定义模型的结构,提供更好的类型安全和智能提示。
### 基本泛型用法
```ts
interface MyModelStructure {
parent?: ParentModel;
subModels?: {
tabs?: TabModel[];
items?: ItemModel[];
};
}
class MyModel extends FlowModel<MyModelStructure> {
// 现在 this.parent 和 this.subModels 都有正确的类型推导
}
```
### 默认结构类型
如果不指定泛型参数FlowModel 使用默认的 `DefaultStructure`
```ts
interface DefaultStructure {
parent?: FlowModel | null;
subModels?: Record<string, FlowModel | FlowModel[]>;
}
```
---
## 主要属性
- **uid: string**
模型唯一标识符。
- **props: IModelComponentProps**
组件属性,支持响应式。
- **stepParams: StepParams**
流步骤参数。
- **flowEngine: FlowEngine**
关联的流引擎实例。
- **parent: Structure['parent']**
父模型实例,类型由泛型 Structure 决定。默认为 `FlowModel | null`
- **subModels: Structure['subModels']**
子模型集合,类型由泛型 Structure 决定。默认为 `Record<string, FlowModel | FlowModel[]>`
支持对象字段(如 detail、config和数组字段如 tabs、columns
---
## 主要方法
### 生命周期与初始化
- **constructor(options)**
创建模型实例,初始化 uid、props、stepParams、subModels 等。
- **onInit(options): void**
可被子类重写的初始化钩子。
---
### 属性与参数管理
- **setProps(props: IModelComponentProps): void**
批量设置属性。
- **setProps(key: string, value: any): void**
设置单个属性。
- **getProps(): ReadonlyModelProps**
获取只读属性。
- **setStepParams(...)**
支持多种重载,设置流步骤参数。
- **getStepParams(...)**
支持多种重载,获取流步骤参数。
---
### 流注册与执行
- **static registerFlow\<TModel\>(keyOrDefinition, flowDefinition?)**
配置流,支持字符串 key 或完整对象。支持泛型以确保类型安全。
- **static extendFlow\<TModel\>(keyOrDefinition, extendDefinition?)**
扩展已存在的流程定义,通过合并现有流程和扩展定义来创建新的流程。
- **applyFlow(flowKey: string, extra?: FlowExtraContext): Promise<any>**
执行指定流。
- **dispatchEvent(eventName: string, extra?: FlowExtraContext): void**
触发事件,自动匹配并执行相关流。
- **applyAutoFlows(extra?: FlowExtraContext): Promise<any[]>**
执行所有自动应用流。
- **getFlow(key: string): FlowDefinition \| undefined**
获取指定 key 的流配置。
- **static getFlows(): Map<string, FlowDefinition>**
获取所有已配置流(含继承)。
- **getAutoFlows(): FlowDefinition[]**
获取所有自动应用流程定义并按 sort 排序。
---
### 步骤设置
- **openStepSettingsDialog(flowKey: string, stepKey: string)**
打开步骤设置对话框。
- **async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string): Promise<any>**
配置必填步骤参数。用于在一个分步表单中配置所有需要参数的步骤。
- `dialogWidth`: 对话框宽度,默认为 800
- `dialogTitle`: 对话框标题,默认为 '步骤参数配置'
- 返回表单提交的值
---
### 子模型管理
- **setSubModel(subKey: string, options): FlowModel**
创建并设置一个子模型到对象字段(如 detail、config
- **addSubModel(subKey: string, options): FlowModel**
创建并添加一个子模型到数组字段(如 tabs、columns
- **mapSubModels<K, R>(subKey: K, callback: (model) => R): R[]**
遍历指定 key 的子模型,对每个子模型执行 callback 函数,并返回结果数组。
- 支持完整的类型推导callback 参数会自动推导为正确的模型类型
- 如果子模型不存在,返回 null
- 自动处理单个模型和模型数组的情况
- **setParent(parent: FlowModel): void**
设置父模型。
- **createRootModel(options): FlowModel**
通过 flowEngine 创建根模型。
---
### 持久化与销毁
- **async save(): Promise<any>**
保存模型到远程。
- **async destroy(): Promise<any>**
删除模型。
---
### 渲染
- **render(): React.ReactNode | Function**
渲染模型的 React 组件,默认返回空 div建议子类重写。
---
## 主要示例
```ts
class MyModel extends FlowModel {
onInit(options) {
// 初始化逻辑
}
render() {
return <div>{this.props.name}</div>;
}
}
// 为 MyModel 配置流
MyModel.registerFlow({ key: 'default', steps: { ... } });
// 创建实例
const model = flowEngine.createModel({ use: 'MyModel', props: { name: 'Demo' } });
// 添加子模型
model.addSubModel('tabs', { use: 'TabFlowModel', props: { label: 'Tab1' } });
// 持久化
await model.save();
// 执行流
await model.applyFlow('default');
await model.applyAutoFlows();
await model.dispatchEvent('event');
```

View File

@ -0,0 +1,217 @@
# FlowResource 及资源体系
`FlowResource` 及其子类用于在流程引擎中管理和操作数据资源。它们封装了数据的获取、设置、同步等常用操作,支持与 API 交互。
## 资源类结构
资源体系的核心类及继承关系如下:
- `FlowResource`:基础资源类,提供数据的基本存取能力。
- `APIResource`:增加了 API 交互能力。
- `BaseRecordResource`:为记录资源提供基础功能,是单条和多条记录资源的基类。
- `SingleRecordResource`:用于管理单个对象(如一条记录)。
- `MultiRecordResource`:用于管理对象数组(如列表、分页数据)。
## 1. FlowResource
基础资源类,提供数据和元信息的响应式存取能力。
### 主要属性
- `_data`: 使用 `observable.ref` 包装的资源数据,存储当前资源的所有字段信息。
- `_meta`: 使用 `observable.ref` 包装的元信息对象,存储如分页、统计等附加信息。
### 主要方法
- `getData()`: 获取当前数据,返回 `_data.value`
- `setData(value)`: 设置当前数据,参数为对象,支持链式调用。
- `getMeta(metaKey?)`: 获取元信息,传入 `metaKey` 时返回对应值,否则返回全部元信息对象。
- `setMeta(meta)`: 合并设置元信息,支持链式调用。
### 示例
```ts
const resource = new FlowResource<{ name: string }>();
resource.setData({ name: '张三' });
console.log(resource.getData()); // { name: '张三' }
resource.setMeta({ page: 1, total: 100 });
console.log(resource.getMeta('page')); // 1
console.log(resource.getMeta()); // { page: 1, total: 100 }
```
### 设计说明
- `FlowResource` 只负责本地数据和元信息的响应式存取,不涉及 API 通信。
- 作为基类,通常被其他资源类继承扩展。
---
## 2. APIResource
继承自 `FlowResource`,为资源增加了 API 通信能力,支持配置 URL、参数、headers并通过 APIClient 拉取数据。
### 主要属性
- `request.url`: 资源的 API 地址,字符串或 null。
- `request.method`: 请求方法(如 'get', 'post' 等),默认为 'get'。
- `request.params`: 请求参数对象。
- `request.headers`: 请求头对象。
- `request.data`: 请求体(可选)。
- `api`: APIClient 实例,用于发起 HTTP 请求。
### 主要方法
- `setAPIClient(api: APIClient)`: 设置 API 客户端实例。
- `getURL()`: 获取当前 API 地址。
- `setURL(value: string)`: 设置 API 地址。
- `setRequestMethod(method: string)`: 设置请求方法。
- `addRequestHeader(key: string, value: string)`: 添加请求头。
- `addRequestParameter(key: string, value: any)`: 添加请求参数。
- `setRequestBody(data: any)`: 设置请求体。
- `setRequestOptions(key: string, value: any)`: 设置 request 对象的任意属性。
- `async refresh()`: 通过 APIClient 拉取数据并更新本地数据GET 请求)。
- `getRefreshRequestOptions(filterByTk?)`: 获取 refresh 请求的参数和 headers支持主键过滤。
### 示例
```ts
const apiResource = new APIResource<{ name: string }>();
apiResource.setAPIClient(apiClientInstance);
apiResource.setURL('/users/1');
apiResource.setRequestMethod('get');
apiResource.addRequestHeader('Authorization', 'Bearer token');
await apiResource.refresh();
console.log(apiResource.getData());
```
### 设计说明
- `APIResource` 负责与后端 API 通信,自动管理数据的获取和本地同步。
- 通过组合 APIClient可灵活适配不同的 API 请求方式和参数。
- 仅实现了 GETrefresh如需扩展可在子类中实现更多操作如 POST、PUT、DELETE
---
## 3. BaseRecordResource
继承自 `APIResource`,为记录资源提供通用的基础功能,是 `SingleRecordResource``MultiRecordResource` 的基类。
### 主要属性
- `resourceName`: 资源名称(如 `users``users.profile` 等)。
- `sourceId`: 源对象 ID用于关联资源如主对象的主键
- `request`: 请求配置对象,包含 `url``method``params``headers` 等,`params` 支持过滤、排序、字段选择、白名单、黑名单等常用 API 参数。
### 主要方法
- `buildURL(action?)`: 构建请求 URL支持关联资源和自定义操作。
- `async runAction(action, options)`: 执行指定操作(如自定义 action通常为 POST 请求。
- `setResourceName(resourceName) / getResourceName()`: 设置/获取资源名称。
- `setSourceId(sourceId) / getSourceId()`: 设置/获取源对象 ID。
- `setDataSourceKey(dataSourceKey) / getDataSourceKey()`: 设置/获取数据源标识(通过 header
- `setFilter(filter) / getFilter()`: 设置/获取过滤条件。
- `setAppends(appends) / getAppends()`: 设置/获取附加字段。
- `addAppends(appends) / removeAppends(appends)`: 添加/移除附加字段。
- `setFilterByTk(filterByTk) / getFilterByTk()`: 设置/获取主键过滤条件。
- `setFields(fields) / getFields()`: 设置/获取字段列表。
- `setSort(sort) / getSort()`: 设置/获取排序字段。
- `setExcept(except) / getExcept()`: 设置/获取排除字段。
- `setWhitelist(whitelist) / getWhitelist()`: 设置/获取白名单字段。
- `setBlacklist(blacklist) / getBlacklist()`: 设置/获取黑名单字段。
- `abstract refresh()`: 抽象方法,需子类实现,用于拉取数据。
### 设计说明
- `BaseRecordResource` 统一封装了常见的 API 参数和资源操作,便于子类扩展。
- 支持链式调用和灵活的参数设置,适配多种业务场景。
- 通过 `buildURL``runAction` 支持 RESTful 及自定义 action 的请求。
### 示例
```ts
const resource = new SomeRecordResource();
resource.setResourceName('users');
resource.setSourceId(1);
resource.setFilter({ status: 'active' });
resource.setFields(['id', 'name']);
await resource.refresh();
console.log(resource.getData());
```
---
## 4. SingleRecordResource
继承自 `BaseRecordResource`,用于管理单个对象(如一条记录),适合详情页、单条数据的增删改查等场景。
### 主要方法
- `setFilterByTk(filterByTk: string | number)`: 设置主键过滤条件(仅接受单个值),用于指定当前操作的对象。
- `async save(data: TData)`: 保存当前对象。若已设置主键(`filterByTk`),则为更新操作,否则为创建操作。保存后自动刷新数据。
- `async destroy()`: 删除当前对象(根据主键),删除后本地数据设为 null。
- `async refresh()`: 拉取单条记录数据并更新本地数据和元信息。
### 设计说明
- 适用于详情页、单条数据的增删改查等场景。
- 通过 `filterByTk` 精确定位单条记录。
- 所有操作均自动同步本地数据和元信息。
### 示例
```ts
const userResource = new SingleRecordResource<{ id: number; name: string }>();
userResource.setResourceName('users');
userResource.setFilterByTk(1);
await userResource.refresh();
console.log(userResource.getData());
await userResource.save({ name: '新名字' });
await userResource.destroy();
```
---
## 5. MultiRecordResource
继承自 `BaseRecordResource`,用于管理对象数组(如列表、分页数据),适合表格、列表页等场景。
### 主要属性
- `_data`: 响应式存储的数据数组,默认为空数组。
- `request.params.page`: 当前页码,默认为 1。
- `request.params.pageSize`: 每页条数,默认为 20。
### 主要方法
- `async next()`: 加载下一页数据。
- `async previous()`: 加载上一页数据(页码大于 1 时)。
- `async goto(page: number)`: 跳转到指定页码。
- `async create(data: TDataItem)`: 创建新对象,成功后自动刷新数据。
- `async update(filterByTk, data)`: 更新指定对象,成功后自动刷新数据。
- `async destroy(filterByTk)`: 删除指定对象(支持单个或多个主键),成功后自动刷新数据。
- `setPage(page: number) / getPage()`: 设置/获取当前页码。
- `setPageSize(pageSize: number) / getPageSize()`: 设置/获取每页条数。
- `async refresh()`: 拉取列表数据并更新本地数据和元信息。
### 设计说明
- 适用于列表、分页、批量操作等场景。
- 所有数据变更操作(增删改)均自动刷新本地数据和元信息。
- 支持链式调用设置分页参数。
### 示例
```ts
const listResource = new MultiRecordResource<{ id: number; name: string }>();
listResource.setResourceName('users');
await listResource.refresh();
console.log(listResource.getData());
await listResource.create({ name: '新用户' });
await listResource.update(1, { name: '更新名' });
await listResource.destroy([1, 2, 3]);
await listResource.next();
await listResource.setPageSize(50).refresh();
```

View File

@ -0,0 +1,6 @@
# FlowSettings
- 悬浮菜单FlowSettingsDropdown
- 右键菜单FlowSettingsContextMenu
- 对话框FlowSettingsModal
- 抽屉FlowSettingsDrawer

View File

@ -0,0 +1 @@
# Overview

View File

@ -0,0 +1,74 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import {
FlowModel,
FlowModelRenderer,
FlowsContextMenu,
FlowsFloatContextMenu,
FlowsSettings,
} from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return <Card>Hello {name}</Card>;
}
}
HelloFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {
name: {
type: 'string',
title: 'Name',
'x-component': Input,
},
},
defaultParams: {
name: 'NocoBase',
},
handler(ctx, params) {
console.log('HelloFlowModel step1 handler', params);
ctx.model.setProps('name', params.name);
},
},
},
});
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
stepParams: {
defaultFlow: {
step1: {
name: 'NocoBase',
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowsContextMenu model={model}>
<FlowModelRenderer model={model} />
</FlowsContextMenu>
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,68 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsFloatContextMenu, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return <Card>Hello {name}</Card>;
}
}
HelloFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {
name: {
type: 'string',
title: 'Name',
'x-component': Input,
},
},
defaultParams: {
name: 'NocoBase',
},
handler(ctx, params) {
console.log('HelloFlowModel step1 handler', params);
ctx.model.setProps('name', params.name);
},
},
},
});
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
stepParams: {
defaultFlow: {
step1: {
name: 'NocoBase',
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowsFloatContextMenu model={model}>
<FlowModelRenderer model={model} />
</FlowsFloatContextMenu>
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,134 @@
import React from 'react';
import { Button, ButtonProps, message, Modal } from 'antd';
import { Application, Plugin } from '@nocobase/client';
import { ActionModel, useFlowModel, withFlowModel, FlowsFloatContextMenu } from '@nocobase/flow-engine';
const ButtonModel = ActionModel.extends([
{
key: 'buttonActionFlow',
title: '按钮操作流程',
on: {
eventName: 'onClick',
},
steps: {
popconfirm: {
title: '确认弹窗',
use: 'showConfirm',
defaultParams: {
title: '确认删除',
message: '确定要删除此记录吗?此操作不可撤销!',
},
},
delete: {
title: '执行删除',
handler: async (ctx) => {
// 模拟API请求
await new Promise((resolve) => setTimeout(resolve, 500));
ctx.message = message;
ctx.message.success('删除成功');
},
},
refresh: {
title: '刷新页面',
handler: () => {
console.log('页面已刷新');
},
},
},
},
{
key: 'default',
patch: true,
steps: {
setText: {
defaultParams: {
text: '删除',
},
},
},
},
]);
// ActionButton 演示组件
const Demo = () => {
const uid = 'delete-button';
const model = useFlowModel(uid, 'ButtonModel');
return (
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
<DeleteButton model={model} />
</div>
);
};
const ButtonComponent = (props: ButtonProps & { text?: string }) => {
const { text, ...rest } = props;
return <Button {...rest}>{text}</Button>;
};
// 使用withFlowModel包装Button组件只启用右键菜单
const DeleteButton = withFlowModel(ButtonComponent, {
settings: {
component: FlowsFloatContextMenu,
props: {
hideRemoveInSettings: true,
},
},
});
// 插件定义
class DemoPlugin extends Plugin {
async load() {
// 1. 注册ButtonModel模型
this.app.flowEngine.registerModels({ ButtonModel });
// 2. 注册确认弹窗Action
this.app.flowEngine.registerAction({
name: 'showConfirm',
title: '显示确认弹窗',
uiSchema: {
title: {
type: 'string',
title: '弹窗标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
message: {
type: 'string',
title: '弹窗内容',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
title: '确认操作',
message: '确定要执行此操作吗?',
},
handler: async (ctx, params) => {
return new Promise((resolve) => {
Modal.confirm({
title: params.title,
content: params.message,
onOk: () => {
resolve(true);
},
onCancel: () => {
resolve(false);
ctx.exit();
},
});
});
},
});
// 注册路由
this.app.router.add('root', { path: '/', Component: Demo });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [DemoPlugin],
});
export default app.getRootComponent();

View File

@ -0,0 +1,102 @@
import { define, observable } from '@formily/reactive';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Input, Space } from 'antd';
import React from 'react';
class ArrayResource {
meta = observable.shallow({
filter: {},
sort: [],
page: 1,
pageSize: 10,
appends: [],
data: [],
});
get data() {
return this.meta.data;
}
set data(value) {
this.meta.data = value;
}
getData() {
return this.meta.data;
}
setData(data) {
this.meta.data = data;
}
async next() {
const newData = Array.from({ length: 3 }, (_, i) => ({
id: Math.floor(Math.random() * 10000),
name: `Item ${Math.floor(Math.random() * 100)}`,
}));
this.setData(newData);
}
async refresh() {
const newData = Array.from({ length: 3 }, (_, i) => ({
id: Math.floor(Math.random() * 10000),
name: `Item ${Math.floor(Math.random() * 100)}`,
}));
this.setData(newData);
}
}
class ArrayResourceFlowModel extends FlowModel {
resource: ArrayResource = new ArrayResource();
render() {
return (
<div>
<pre>{JSON.stringify(this.resource.getData(), null, 2)}</pre>
<Space>
<Button
onClick={() => {
this.resource.refresh();
}}
>
Refresh
</Button>
<Button
onClick={() => {
this.resource.next();
}}
>
Next
</Button>
</Space>
</div>
);
}
}
// 插件定义
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ ArrayResourceFlowModel });
const model = this.flowEngine.createModel({
use: 'ArrayResourceFlowModel',
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,106 @@
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { DataSource, DataSourceManager, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Card, Space } from 'antd';
import React from 'react';
const dsm = new DataSourceManager();
const ds = new DataSource({
name: 'main',
displayName: 'Main',
description: 'This is the main data source',
});
dsm.addDataSource(ds);
ds.addCollection({
name: 'base',
title: 'base',
fields: [
{
name: 'id',
type: 'string',
title: 'ID',
},
],
});
ds.addCollection({
name: 'users',
title: 'Users',
fields: [
{
name: 'name',
type: 'string',
title: 'Name',
},
],
});
ds.addCollection({
name: 'students',
title: 'Students',
inherits: ['base', 'users'],
fields: [
{
name: 'age',
type: 'integer',
title: 'Age',
},
],
});
class ConfigureFieldsFlowModel extends FlowModel {
get collection() {
return ds.getCollection('students');
}
render() {
return (
<div>
<Card key={this.collection.name} title={this.collection.title} style={{ marginBottom: 24 }}>
{this.collection.getFields().map((field) => (
<div key={field.name}>{field.name}</div>
))}
<Space>
<Button
onClick={() => {
this.collection.addField({
name: `field-${uid()}`,
});
}}
>
Add Field
</Button>
<Button
onClick={() => {
this.collection.clearFields();
}}
>
Clear Fields
</Button>
</Space>
</Card>
</div>
);
}
}
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ ConfigureFieldsFlowModel });
const model = this.flowEngine.createModel({
use: 'ConfigureFieldsFlowModel',
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} />,
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,189 @@
import { Application, Plugin } from '@nocobase/client';
import { Collection, DataSource, DataSourceManager, Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Dropdown, Input } from 'antd';
import React from 'react';
const dsm = new DataSourceManager();
const ds = new DataSource({
name: 'main',
displayName: 'Main',
description: 'This is the main data source',
});
dsm.addDataSource(ds);
ds.addCollection({
name: 'roles',
title: 'Roles',
fields: [
{
name: 'name',
type: 'string',
title: 'Name',
},
{
name: 'uid',
type: 'string',
title: 'UID',
},
],
});
ds.addCollection({
name: 'users',
title: 'Users',
fields: [
{
name: 'username',
type: 'string',
title: 'Username',
},
{
name: 'nickname',
type: 'string',
title: 'Nickname',
},
],
});
class FieldModel extends FlowModel {
field: Field;
render() {
return (
<div>
<Input
value={this.field.title}
onChange={(e) => {
const field = dsm.getCollectionField(this.stepParams.default.step1.fieldPath);
if (!field) {
console.error('Field not found:', this.stepParams.default.step1.fieldPath);
return;
}
field.title = e.target.value;
}}
/>
</div>
);
}
}
FieldModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
handler(ctx, params) {
if (ctx.model.field) {
return;
}
ctx.model.field = dsm.getCollectionField(params.fieldPath);
},
},
},
});
type S = {
subModels: {
fields: FieldModel[];
};
};
class ConfigureFieldsFlowModel extends FlowModel<S> {
collection: Collection;
getFieldMenuItems() {
return this.collection.mapFields((field) => {
return {
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
label: field.title,
};
});
}
render() {
return (
<div>
{this.mapSubModels('fields', (field) => (
<FlowModelRenderer key={field.uid} model={field} />
))}
<Dropdown
menu={{
items: this.getFieldMenuItems(),
onClick: (info) => {
const model = this.addSubModel('fields', {
use: 'FieldModel',
stepParams: {
default: {
step1: {
fieldPath: info.key,
},
},
},
});
},
}}
>
<Button>Configure fields</Button>
</Dropdown>
</div>
);
}
}
ConfigureFieldsFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'DataSource Name',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
collectionName: {
type: 'string',
title: 'Collection Name',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
handler(ctx, params) {
if (ctx.model.collection) {
return;
}
ctx.model.collection = dsm.getCollection(params.dataSourceKey, params.collectionName);
},
},
},
});
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ FieldModel, ConfigureFieldsFlowModel });
const model = this.flowEngine.createModel({
use: 'ConfigureFieldsFlowModel',
stepParams: {
default: {
step1: {
dataSourceKey: 'main',
collectionName: 'users',
},
},
},
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} showFlowSettings />,
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

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

View File

@ -0,0 +1,112 @@
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { DataSource, DataSourceManager, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Card, Space } from 'antd';
import React from 'react';
const dsm = new DataSourceManager();
const ds = new DataSource({
name: 'main',
displayName: 'Main',
description: 'This is the main data source',
});
dsm.addDataSource(ds);
class ConfigureFieldsFlowModel extends FlowModel {
getDataSources() {
return [...dsm.dataSources.values()];
}
render() {
return (
<div>
{this.getDataSources().map((ds) => (
<Card key={ds.name} title={ds.options.displayName} style={{ marginBottom: 24 }}>
{ds.getCollections().map((collection) => (
<Card key={collection.name} title={collection.title} style={{ marginBottom: 24 }}>
{collection.getFields().map((field) => (
<div key={field.name}>{field.name}</div>
))}
<Space>
<Button
onClick={() => {
collection.addField({
name: `field-${uid()}`,
});
}}
>
Add Field
</Button>
<Button
onClick={() => {
collection.clearFields();
}}
>
Clear Fields
</Button>
</Space>
</Card>
))}
<Space>
<Button
onClick={() => {
ds.addCollection({
name: `collection-${uid()}`,
title: `Collection ${uid()}`,
});
}}
>
Add Collection
</Button>
<Button
onClick={() => {
ds.clearCollections();
}}
>
Clear Collection
</Button>
</Space>
</Card>
))}
<Space>
<Button
onClick={() => {
dsm.addDataSource({
name: `ds-${uid()}`,
displayName: `ds-${uid()}`,
});
}}
>
Add Data Source
</Button>
<Button
onClick={() => {
dsm.clearDataSources();
}}
>
Clear Data Sources
</Button>
</Space>
</div>
);
}
}
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ ConfigureFieldsFlowModel });
const model = this.flowEngine.createModel({
use: 'ConfigureFieldsFlowModel',
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} />,
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,55 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return (
<div
onClick={(event) => {
this.dispatchEvent('event1', { event });
}}
>
Hello {name}
</div>
);
}
}
HelloFlowModel.registerFlow({
key: 'default',
on: {
eventName: 'event1',
},
steps: {
step1: {
async handler(ctx, params) {
ctx.logger.info('Event triggered with ctx:', ctx);
// alert(`Event triggered with params`);
},
},
},
});
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
props: {
name: 'NocoBase',
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,128 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React, { createRef } from 'react';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
function check() {
if (ref.current) return cb(ref.current);
if (Date.now() - start > timeout) return;
setTimeout(check, 30);
}
check();
}
class RefFlowModel extends FlowModel {
ref = createRef<HTMLDivElement>();
render() {
return (
<Card>
<div ref={this.ref} style={{ width: '100%', height: 400 }} />
</Card>
);
}
}
RefFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step0: {
use: 'require',
defaultParams: {
paths: {
requireEcharts: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min',
},
},
},
step1: {
uiSchema: {
option: {
type: 'string',
title: 'ECharts 配置',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,
},
},
},
async handler(ctx, params) {
waitForRefCallback(ctx.model.ref, async (el) => {
const echarts = await ctx.globals.requireAsync('requireEcharts');
const chart = echarts.init(el);
chart.setOption(JSON.parse(params.option));
});
},
},
},
});
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.setContext({
requireAsync: async (mod) => {
return new Promise((resolve, reject) => {
this.app.requirejs.requirejs([mod], (arg) => resolve(arg), reject);
});
},
});
this.flowEngine.registerAction('require', {
handler: (ctx, params) => {
this.app.requirejs.requirejs.config({
paths: params.paths,
});
},
});
this.flowEngine.registerModels({ RefFlowModel });
const model = this.flowEngine.createModel({
use: 'RefFlowModel',
stepParams: {
defaultFlow: {
step1: {
option: JSON.stringify(
{
title: {
text: 'ECharts 示例',
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
},
null,
2,
),
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
<br />
<FlowsSettings model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,49 @@
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 || 'Action'}</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 () => {},
});
},
},
},
});

View File

@ -0,0 +1,42 @@
import { FormItem, Input } from '@formily/antd-v5';
import { Field as FormilyField } from '@formily/react';
import { Field, FlowModel } from '@nocobase/flow-engine';
import React from 'react';
export class FormItemModel extends FlowModel {
field: Field;
render() {
return (
<div>
<FormilyField
name={this.field.name}
title={this.field.title}
required
decorator={[FormItem]}
component={[
Input,
{
style: {
width: 240,
},
},
]}
/>
</div>
);
}
}
FormItemModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
handler(ctx, params) {
const field = ctx.globals.dsm.getCollectionField(params.fieldPath);
ctx.model.field = field;
},
},
},
});

View File

@ -0,0 +1,99 @@
import { FormButtonGroup, FormDialog, FormItem, Input, Submit } from '@formily/antd-v5';
import { createForm, Form } from '@formily/core';
import { FormProvider } from '@formily/react';
import {
Collection,
FlowEngineProvider,
FlowModel,
FlowModelRenderer,
SingleRecordResource,
} from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
import { api } from '../table/api';
export class FormModel extends FlowModel {
form: Form;
resource: SingleRecordResource;
collection: Collection;
render() {
return (
<div>
<FormProvider form={this.form}>
{this.mapSubModels('fields', (field) => (
<FlowModelRenderer model={field} />
))}
<FormButtonGroup>
{this.mapSubModels('actions', (action) => (
<FlowModelRenderer model={action} />
))}
</FormButtonGroup>
<br />
<Card>
<pre>{JSON.stringify(this.form.values, null, 2)}</pre>
</Card>
</FormProvider>
</div>
);
}
async openDialog({ filterByTk }) {
return new Promise((resolve) => {
const dialog = FormDialog(
{
footer: null,
title: 'Form Dialog',
},
(form) => {
return (
<div>
<FlowEngineProvider engine={this.flowEngine}>
<FlowModelRenderer model={this} extraContext={{ form, filterByTk }} />
<FormButtonGroup>
<Submit
onClick={async () => {
await this.resource.save(this.form.values);
dialog.close();
resolve(this.form.values); // 在 close 之后 resolve
}}
>
Submit
</Submit>
</FormButtonGroup>
</FlowEngineProvider>
</div>
);
},
);
dialog.open();
// 可选:如果需要在取消时也 resolve可以监听 dialog 的 onCancel
});
}
}
FormModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
async handler(ctx, params) {
ctx.model.form = ctx.extra.form || createForm();
if (ctx.model.collection) {
return;
}
ctx.model.collection = ctx.globals.dsm.getCollection(params.dataSourceKey, params.collectionName);
const resource = new SingleRecordResource();
resource.setDataSourceKey(params.dataSourceKey);
resource.setResourceName(params.collectionName);
resource.setAPIClient(api);
ctx.model.resource = resource;
if (ctx.extra.filterByTk) {
resource.setFilterByTk(ctx.extra.filterByTk);
await resource.refresh();
ctx.model.form.setInitialValues(resource.getData());
}
},
},
},
});

View File

@ -0,0 +1,66 @@
import { Plugin } from '@nocobase/client';
import { ActionModel, FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
import { createApp } from '../createApp';
import { dsm } from '../table/data-source-manager';
import { FormItemModel } from './form-item-model';
import { FormModel } from './form-model';
import { SubmitActionModel } from './submit-action-model';
class PluginDemo extends Plugin {
async load() {
this.flowEngine.context.dsm = dsm;
this.flowEngine.registerModels({
FormModel,
FormItemModel,
ActionModel,
SubmitActionModel,
});
const model = this.flowEngine.createModel({
use: 'FormModel',
stepParams: {
default: {
step1: {
dataSourceKey: 'main',
collectionName: 'users',
},
},
},
subModels: {
fields: [
{
use: 'FormItemModel',
stepParams: {
default: {
step1: {
fieldPath: 'main.users.username',
},
},
},
},
{
use: 'FormItemModel',
stepParams: {
default: {
step1: {
fieldPath: 'main.users.nickname',
},
},
},
},
],
actions: [
{
use: 'SubmitActionModel',
},
],
},
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} extraContext={{ filterByTk: 1 }} />,
});
}
}
export default createApp({ plugins: [PluginDemo] });

View File

@ -0,0 +1,28 @@
import { Submit } from '@formily/antd-v5';
import React from 'react';
import { ActionModel } from './action-model';
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();
}
},
},
},
});

View File

@ -0,0 +1,130 @@
import { FormButtonGroup, FormItem, Input, Submit } from '@formily/antd-v5';
import { createForm, Form } from '@formily/core';
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
const schema: ISchema = {
type: 'object',
properties: {
input: {
type: 'string',
title: 'input box',
'x-decorator': 'FormItem',
'x-component': 'Input',
default: 'Hello, NocoBase!',
required: true,
'x-component-props': {
style: {
width: 240,
},
},
},
textarea: {
type: 'string',
title: 'text box',
required: true,
default: 'This is a text box.',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
style: {
width: 400,
},
},
},
},
};
class FormilyFlowModel extends FlowModel {
SchemaField: any;
form: Form;
onInit(options: any): void {
this.SchemaField = createSchemaField({
components: {
Input,
FormItem,
},
});
this.form = createForm();
}
render() {
return (
<FormProvider form={this.form}>
<this.SchemaField schema={this.props.schema} />
<FormButtonGroup>
<Submit onSubmit={console.log}>Submit</Submit>
</FormButtonGroup>
<br />
<Card>
<pre>{JSON.stringify(this.form.values, null, 2)}</pre>
</Card>
</FormProvider>
);
}
}
FormilyFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {
schema: {
type: 'string',
title: 'Formily Schema',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,
},
},
},
async handler(ctx, params) {
try {
ctx.model.setProps('schema', JSON.parse(params.schema));
ctx.model.form.clearFormGraph();
} catch (error) {
// skip
}
},
},
},
});
class PluginFormilyModel extends Plugin {
async load() {
this.flowEngine.registerModels({ FormilyFlowModel });
const model = this.flowEngine.createModel({
use: 'FormilyFlowModel',
// props: {
// schema,
// },
stepParams: {
defaultFlow: {
step1: {
schema: JSON.stringify(schema, null, 2),
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
<br />
<FlowsSettings model={model} />
</div>
),
});
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginFormilyModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,59 @@
import React from 'react';
interface GridProps {
items: string[][][]; // 三维数组:行-列-区块
itemRender: (uid: string) => React.ReactNode;
}
function Grid({ items, itemRender }: GridProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{items.map((row, rowIdx) => (
<div key={rowIdx} style={{ display: 'flex', gap: 8 }}>
{row.map((col, colIdx) => (
<div
key={colIdx}
style={{
width: '100%',
display: 'flex',
flexDirection: 'column',
gap: 4,
border: '1px solid #eee',
padding: 8,
}}
>
{col.map((uid, blockIdx) => (
<div style={{ width: '100%' }} key={blockIdx}>
{itemRender(uid)}
</div>
))}
</div>
))}
</div>
))}
</div>
);
}
const items = [
[
// 第一行
['A', 'B'], // 第一行的第一列两个区块
['C'], // 第一行的第二列一个区块
],
[
// 第二行
['D'], // 第二行的第一列一个区块
['E', 'F'], // 第二行的第二列两个区块
],
];
function GridDemo() {
return (
<div>
<Grid items={items} itemRender={(uid) => <div style={{ background: '#f5f5f5', padding: 4 }}>{uid}</div>} />
</div>
);
}
export default GridDemo;

View File

@ -0,0 +1,45 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Input } from 'antd';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return (
<div>
<div style={{ margin: 10 }}>
Hello <strong>{name}</strong>
</div>
<Input
defaultValue={name}
onChange={(e) => {
this.props.name = e.target.value;
}}
/>
</div>
);
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
props: {
name: 'NocoBase',
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,32 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return <div>Hello {name}</div>;
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
props: {
name: 'NocoBase',
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,41 @@
/**
* iframe: true
* compact: true
*/
import ProLayout from '@ant-design/pro-layout';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
class LayoutFlowModel extends FlowModel {
render() {
const { name } = this.props;
return (
<div>
<ProLayout />
</div>
);
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ LayoutFlowModel });
const model = this.flowEngine.createModel({
use: 'LayoutFlowModel',
props: {
name: 'NocoBase',
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,144 @@
import React from 'react';
import { Application, Plugin } from '@nocobase/client';
import { useFlowModel, FlowContext, BlockModel, withFlowModel, FlowsSettings } from '@nocobase/flow-engine';
import MarkdownIt from 'markdown-it';
import Handlebars from 'handlebars';
const Demo = () => {
const uid = 'markdown-block';
const model = useFlowModel<BlockModel>(uid, 'MarkdownModel');
return (
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
<MarkdownBlock model={model} />
</div>
);
};
const Markdown = ({ content, height }) => {
if (content === undefined || content === null) {
return <div style={{ height: height || 100 }}>Loading content or no content set...</div>;
}
return <div dangerouslySetInnerHTML={{ __html: content }} style={{ height }} />;
};
const MarkdownBlock = withFlowModel(Markdown, {
settings: {
component: FlowsSettings,
props: {
expandAll: true,
},
},
});
const MarkdownModel = BlockModel.extends([
{
key: 'default',
title: 'Markdown',
auto: true,
steps: {
setTemplate: {
use: 'block:markdown:template',
title: '模板引擎',
defaultParams: { template: 'plain' },
},
setHeight: {
use: 'block:markdown:height',
title: '高度',
defaultParams: { height: 300 },
},
setContent: {
use: 'block:markdown:content',
title: '内容',
defaultParams: { content: 'Hello, NocoBase! {{var1}}' },
},
renderMarkdown: {
handler: async (ctx: FlowContext) => {
const props = ctx.model.getProps();
let content = props.content;
if (props.template === 'handlebars') {
content = Handlebars.compile(content || '')({
var1: 'variable 1',
var2: 'variable 2',
var3: 'variable 3',
});
}
ctx.model.setProps('content', MarkdownIt().render(content || ''));
},
},
},
},
]);
class DemoPlugin extends Plugin {
async load() {
this.app.flowEngine.registerModels({ MarkdownModel });
this.app.flowEngine.registerAction({
name: 'block:markdown:template',
title: '模板引擎',
uiSchema: {
template: {
type: 'string',
title: '模板类型',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: '普通文本', value: 'plain' },
{ label: 'Handlebars模板', value: 'handlebars' },
],
},
},
defaultParams: { template: 'plain' },
handler: (ctx: FlowContext, params: any) => {
if (params?.template != null) {
ctx.model.setProps('template', params.template);
}
},
});
this.app.flowEngine.registerAction({
name: 'block:markdown:height',
title: '高度设置',
uiSchema: {
height: {
type: 'number',
title: '高度设置',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': { addonAfter: 'px' },
},
},
handler: (ctx: FlowContext, params: any) => {
if (params?.height != null) {
ctx.model.setProps('height', params.height);
}
},
});
this.app.flowEngine.registerAction({
name: 'block:markdown:content',
title: '内容设置',
uiSchema: {
content: {
type: 'string',
title: 'Markdown内容',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
handler: (ctx: FlowContext, params: any) => {
ctx.model.setProps('content', params?.content);
},
});
this.app.router.add('root', { path: '/', Component: Demo });
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [DemoPlugin],
});
export default app.getRootComponent();

View File

@ -0,0 +1,45 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, IFlowModelRepository } from '@nocobase/flow-engine';
import React from 'react';
class FlowModelRepository implements IFlowModelRepository<FlowModel> {
constructor(private app: Application) {}
async load(uid: string) {
// implement fetching a model by id
return null;
}
async save(model: FlowModel) {
console.log('Saving model:', model);
// implement saving a model
return model;
}
async destroy(uid: string) {
// implement deleting a model by id
return true;
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.setModelRepository(new FlowModelRepository(this.app));
this.flowEngine.registerModels({ FlowModel });
const model = this.flowEngine.createModel({
use: 'FlowModel',
props: {
name: 'NocoBase',
},
});
await model.save();
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,147 @@
import { Application, Plugin } from '@nocobase/client';
import { Collection, DataSource, DataSourceManager, Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Dropdown, Input, Table } from 'antd';
import React from 'react';
const dsm = new DataSourceManager();
const ds = new DataSource({
name: 'main',
displayName: 'Main',
description: 'This is the main data source',
});
dsm.addDataSource(ds);
ds.addCollection({
name: 'users',
title: 'Users',
fields: [
{
name: 'id',
type: 'bigInt',
title: 'ID',
},
{
name: 'username',
type: 'string',
title: 'Username',
},
{
name: 'nickname',
type: 'string',
title: 'Nickname',
},
],
});
class FieldModel extends FlowModel {
field: Field;
render() {
return (
<div>
<Input
value={this.field.title}
onChange={(e) => {
const field = dsm.getCollectionField(this.stepParams.default.step1.fieldPath);
if (!field) {
console.error('Field not found:', this.stepParams.default.step1.fieldPath);
return;
}
field.title = e.target.value;
}}
/>
</div>
);
}
}
FieldModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
handler(ctx, params) {
if (ctx.model.field) {
return;
}
ctx.model.field = dsm.getCollectionField(params.fieldPath);
},
},
},
});
type S = {
subModels: {
fields: FieldModel[];
};
};
class ConfigureFieldsFlowModel extends FlowModel<S> {
collection: Collection;
render() {
return (
<div>
<Table />
</div>
);
}
}
ConfigureFieldsFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'DataSource Name',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
collectionName: {
type: 'string',
title: 'Collection Name',
'x-component': 'Input',
'x-decorator': 'FormItem',
},
},
handler(ctx, params) {
if (ctx.model.collection) {
return;
}
ctx.model.collection = dsm.getCollection(params.dataSourceKey, params.collectionName);
},
},
},
});
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ FieldModel, ConfigureFieldsFlowModel });
const model = this.flowEngine.createModel({
use: 'ConfigureFieldsFlowModel',
stepParams: {
default: {
step1: {
dataSourceKey: 'main',
collectionName: 'users',
},
},
},
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} showFlowSettings />,
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,241 @@
import { faker } from '@faker-js/faker';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, MultiRecordResource } from '@nocobase/flow-engine';
import { Button, Space } from 'antd';
import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { APIClient } from '@nocobase/sdk';
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
const mock = new MockAdapter(api.axios);
const records = [
{
id: 1,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
},
{
id: 2,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
},
{
id: 3,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
},
{
id: 4,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
},
{
id: 5,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
},
];
mock.onGet('posts:list').reply((config) => {
const page = parseInt(config.params?.page) || 1;
const pageSize = parseInt(config.params?.pageSize) || 3;
const start = (page - 1) * pageSize;
const items = records.slice(start, start + pageSize);
return [
200,
{
data: items,
meta: { page, pageSize, total: records.length },
},
];
});
mock.onPost('posts:create').reply((config) => {
const newItem = {
...JSON.parse(config.data),
id: Math.max(...records.map(r => r.id)) + 1,
};
records.push(newItem);
return [
200,
{
data: newItem,
},
];
});
mock.onPost('posts:update').reply((config) => {
const filterByTk = config.params?.filterByTk;
const index = records.findIndex((item) => item.id === filterByTk);
if (index === -1) {
return [
404,
{
errors: [
{
code: 'NotFound',
message: 'Record not found',
},
],
},
];
}
records[index] = {
...records[index],
...JSON.parse(config.data),
};
return [
200,
{
data: records[index],
},
];
});
mock.onPost('posts:destroy').reply((config) => {
const filterByTk = config.params?.filterByTk;
const index = records.findIndex((item) => item.id === filterByTk);
if (index === -1) {
return [
404,
{
errors: [
{
code: 'NotFound',
message: 'Record not found',
},
],
},
];
}
const deleted = records.splice(index, 1)[0];
return [
200,
{
data: deleted,
},
];
});
class MultiRecordFlowModel extends FlowModel {
resource = new MultiRecordResource();
render() {
const data = this.resource.getData() || [];
// 从 state 中获取 meta 信息,这是在 refresh 时设置的
const responseMeta = this.resource.getMeta() || {};
return (
<div>
<div style={{ marginBottom: 16 }}>
<strong>Resource:</strong> {this.resource.getResourceName()} |
<strong> Page:</strong> {this.resource.getPage()} |
<strong> PageSize:</strong> {this.resource.getPageSize()} |
<strong> Total:</strong> {responseMeta.total || 0}
</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
<Space wrap>
<Button
onClick={() => {
// 重置数据
records.splice(0, records.length);
for (let i = 1; i <= 5; i++) {
records.push({
id: i,
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
});
}
this.resource.refresh();
}}
>
Reset
</Button>
<Button onClick={() => this.resource.refresh()}>Refresh</Button>
<Button onClick={() => this.resource.next()}>Next Page</Button>
<Button onClick={() => this.resource.previous()}>Previous Page</Button>
<Button onClick={() => this.resource.goto(1)}>Go to Page 1</Button>
<Button
onClick={() =>
this.resource.create({
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
})
}
>
Create
</Button>
<Button
onClick={() => {
const firstItem = data[0];
if (firstItem) {
this.resource.update(firstItem.id, {
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
});
}
}}
>
Update First
</Button>
<Button
onClick={() => {
const firstItem = data[0];
if (firstItem) {
this.resource.destroy(firstItem.id);
}
}}
>
Delete First
</Button>
</Space>
</div>
);
}
}
MultiRecordFlowModel.registerFlow({
auto: true,
key: 'setResourceOptions',
steps: {
step1: {
async handler(ctx, params) {
ctx.model.resource.setAPIClient(api);
ctx.model.resource.setResourceName('posts');
ctx.model.resource.setPage(1);
ctx.model.resource.setPageSize(3);
await ctx.model.resource.refresh();
},
},
},
});
class PluginMultiRecordDemo extends Plugin {
async load() {
this.flowEngine.registerModels({ MultiRecordFlowModel });
const model = this.flowEngine.createModel({
use: 'MultiRecordFlowModel',
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
</div>
),
});
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginMultiRecordDemo],
});
export default app.getRootComponent();

View File

@ -0,0 +1,82 @@
import { define, observable } from '@formily/reactive';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Input } from 'antd';
import React from 'react';
class ObjectResource {
meta = observable.shallow({
filter: {},
filterByTk: null,
appends: [],
data: {},
});
get data() {
return this.meta.data;
}
set data(value) {
this.meta.data = value;
}
getData() {
return this.meta.data;
}
setData(data) {
this.meta.data = data;
}
async refresh() {
this.setData({
id: Math.floor(Math.random() * 10000),
name: `Item ${Math.floor(Math.random() * 100)}`,
});
}
}
class ObjectResourceFlowModel extends FlowModel {
resource: ObjectResource = new ObjectResource();
render() {
return (
<div>
<pre>{JSON.stringify(this.resource.getData(), null, 2)}</pre>
<Button
onClick={() => {
this.resource.refresh();
}}
>
Refresh
</Button>
</div>
);
}
}
// 插件定义
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ ObjectResourceFlowModel });
const model = this.flowEngine.createModel({
use: 'ObjectResourceFlowModel',
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,54 @@
import { observable } from '@formily/reactive';
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Tabs } from 'antd';
import React from 'react';
class HelloFlowModel extends FlowModel {
tabs: Array<any>;
onInit(options: any) {
this.tabs = observable(options.tabs || []);
}
addTab(tab: any) {
this.tabs.push(tab);
}
render() {
return (
<div>
<Tabs
items={this.tabs.slice()}
tabBarExtraContent={
<Button onClick={() => this.addTab({ key: uid(), label: `Tab -${uid()}` })}>Add Tab</Button>
}
/>
</div>
);
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel<HelloFlowModel>({
use: 'HelloFlowModel',
tabs: [
{ key: uid(), label: 'Tab 1' },
{ key: uid(), label: 'Tab 2' },
],
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,201 @@
import { Input, Select } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Button, Card, Space, message } from 'antd';
import React, { useState } from 'react';
class DemoFlowModel extends FlowModel {}
// 注册包含必填参数的流程
DemoFlowModel.registerFlow('configFlow', {
auto: true,
title: '配置流程',
steps: {
// 数据源配置 - 必填参数
setDataSource: {
title: '数据源配置',
paramsRequired: true,
uiSchema: {
dataSource: {
type: 'string',
title: '数据源',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: '主数据源', value: 'main' },
{ label: '用户数据源', value: 'users' },
{ label: '订单数据源', value: 'orders' },
{ label: '产品数据源', value: 'products' },
],
required: true,
},
},
defaultParams: {
dataSource: 'main',
},
handler(ctx, params) {
console.log('设置数据源:', params);
ctx.model.setProps('dataSource', params.dataSource);
},
},
// 数据表配置 - 必填参数
setCollection: {
title: '数据表配置',
paramsRequired: true,
uiSchema: {
collection: {
type: 'string',
title: '数据表',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: '用户表', value: 'users' },
{ label: '角色表', value: 'roles' },
{ label: '权限表', value: 'permissions' },
{ label: '部门表', value: 'departments' },
],
required: true,
},
},
defaultParams: {
collection: 'users',
},
handler(ctx, params) {
console.log('设置数据表:', params);
ctx.model.setProps('collection', params.collection);
},
},
// 标题配置 - 必填参数
setTitle: {
title: '标题配置',
paramsRequired: true,
uiSchema: {
title: {
type: 'string',
title: '区块标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
required: true,
},
subtitle: {
type: 'string',
title: '副标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
defaultParams: {
title: '数据区块',
subtitle: '',
},
handler(ctx, params) {
console.log('设置标题:', params);
ctx.model.setProps('title', params.title);
ctx.model.setProps('subtitle', params.subtitle);
},
},
// 可选配置 - 不是必填参数
setOptionalConfig: {
title: '可选配置',
paramsRequired: false,
uiSchema: {
showBorder: {
type: 'boolean',
title: '显示边框',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
extraInfo: {
type: 'string',
title: '额外信息',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
showBorder: true,
extraInfo: '可选信息',
},
handler(ctx, params) {
console.log('设置可选配置:', params);
ctx.model.setProps('showBorder', params.showBorder);
ctx.model.setProps('extraInfo', params.extraInfo);
},
},
},
});
// 演示组件
const Demo = () => {
const [model, setModel] = useState(null);
const handleCreateModel = async () => {
try {
// 创建一个新的模型实例
const model = app.flowEngine.createModel({
use: 'DemoFlowModel',
uid: `configurable-model-${Date.now()}`,
});
const result = await model.configureRequiredSteps();
message.success('参数配置完成!');
console.log('configuration:', model.stepParams);
setModel(model);
} catch (error) {
console.error('配置过程中出现错误:', error);
message.error('配置过程中出现错误');
}
};
return (
<div style={{ padding: 24, background: '#f5f5f5', borderRadius: 8 }}>
<Card title="分步表单参数配置演示">
<p>
<code>configureRequiredSteps</code> 使
<code>paramsRequired: true</code>
</p>
<Space direction="vertical" style={{ width: '100%' }}>
<Button type="primary" onClick={handleCreateModel}>
</Button>
<div style={{ marginTop: 16 }}>
<h4></h4>
<ul>
<li><strong>setDataSource</strong>: ()</li>
<li><strong>setCollection</strong>: ()</li>
<li><strong>setTitle</strong>: ()</li>
<li><strong>setOptionalConfig</strong>: ()</li>
</ul>
<p>
"创建模型并配置必填参数"
</p>
{model && JSON.stringify(model.stepParams || {})}
</div>
</Space>
</Card>
</div>
);
};
// 插件定义
class DemoPlugin extends Plugin {
async load() {
// 注册模型
this.app.flowEngine.registerModels({ DemoFlowModel });
// 注册路由
this.app.router.add('root', { path: '/', Component: Demo });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [DemoPlugin],
});
export default app.getRootComponent();

View File

@ -0,0 +1,40 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button } from 'antd';
import React from 'react';
// 自定义模型类,继承自 FlowModel
class MyModel extends FlowModel {
render() {
return <Button {...this.props} />;
}
}
// 插件类,负责注册模型、仓库,并加载或创建模型实例
class PluginHelloModel extends Plugin {
async load() {
// 注册自定义模型
this.flowEngine.registerModels({ MyModel });
const model = this.flowEngine.createModel({
uid: 'my-model',
use: 'MyModel',
props: {
type: 'primary',
children: 'Primary Button',
},
});
// 注册路由,渲染模型
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} />,
});
}
}
// 创建应用实例,注册插件
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,105 @@
import * as icons from '@ant-design/icons';
import { FormItem, FormLayout, Input, Select } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button } from 'antd';
import React from 'react';
// 自定义模型类,继承自 FlowModel
class MyModel extends FlowModel {
render() {
console.log('Rendering MyModel with props:', this.props);
return <Button {...this.props} />;
}
}
const myPropsFlow = defineFlow({
key: 'myPropsFlow',
auto: true,
title: '按钮配置',
steps: {
setProps: {
title: '按钮属性设置',
uiSchema: {
title: {
type: 'string',
title: '按钮标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
type: {
type: 'string',
title: '类型',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: '主要', value: 'primary' },
{ label: '次要', value: 'default' },
{ label: '危险', value: 'danger' },
{ label: '虚线', value: 'dashed' },
{ label: '链接', value: 'link' },
{ label: '文本', value: 'text' },
],
},
icon: {
type: 'string',
title: '图标',
'x-decorator': 'FormItem',
'x-component': 'Select',
enum: [
{ label: '搜索', value: 'SearchOutlined' },
{ label: '添加', value: 'PlusOutlined' },
{ label: '删除', value: 'DeleteOutlined' },
{ label: '编辑', value: 'EditOutlined' },
{ label: '设置', value: 'SettingOutlined' },
],
},
},
defaultParams: {
type: 'primary',
},
// 步骤处理函数,设置模型属性
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 PluginHelloModel extends Plugin {
async load() {
// 注册自定义模型
this.flowEngine.registerModels({ MyModel });
const model = this.flowEngine.createModel({
uid: 'my-model',
use: 'MyModel',
stepParams: {
myPropsFlow: {
setProps: {
title: 'Primary Button',
type: 'primary',
},
},
},
});
// 注册路由,渲染模型
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} showFlowSettings />,
});
}
}
// 创建应用实例,注册插件
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,149 @@
import * as icons from '@ant-design/icons';
import { FormItem, Input, Select } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { defineFlow, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Modal } from 'antd';
import React from 'react';
// 自定义模型类,继承自 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',
},
// 步骤处理函数,设置模型属性
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);
},
},
},
});
const myEventFlow = defineFlow({
key: 'myEventFlow',
on: {
eventName: 'onClick',
},
title: '按钮事件',
steps: {
confirm: {
title: '确认操作配置',
uiSchema: {
title: {
type: 'string',
title: '弹窗提示标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
content: {
type: 'string',
title: '弹窗提示内容',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
title: '确认操作',
content: '你点击了按钮,是否确认?',
},
handler(ctx, params) {
Modal.confirm({
...params,
});
},
},
},
});
MyModel.registerFlow(myPropsFlow);
MyModel.registerFlow(myEventFlow);
// 插件类,负责注册模型、仓库,并加载或创建模型实例
class PluginHelloModel extends Plugin {
async load() {
// 注册自定义模型
this.flowEngine.registerModels({ MyModel });
const model = this.flowEngine.createModel({
uid: 'my-model',
use: 'MyModel',
stepParams: {
myPropsFlow: {
setProps: {
title: 'Primary Button',
type: 'primary',
},
},
},
});
// 注册路由,渲染模型
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} showFlowSettings />,
});
}
}
// 创建应用实例,注册插件
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,61 @@
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import moment from 'moment';
import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import 'react-big-calendar/lib/css/react-big-calendar.css';
const events = [
{
id: 0,
title: '会议',
start: new Date(),
end: new Date(new Date().getTime() + 60 * 60 * 1000),
},
{
id: 1,
title: '演示',
start: new Date(new Date().getTime() + 2 * 60 * 60 * 1000),
end: new Date(new Date().getTime() + 3 * 60 * 60 * 1000),
},
];
const localizer = momentLocalizer(moment);
class HelloFlowModel extends FlowModel {
render() {
return (
<div style={{ height: 500 }}>
<Calendar
localizer={localizer}
events={this.props.events || []}
startAccessor="start"
endAccessor="end"
style={{ height: 500 }}
/>
</div>
);
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
props: {
events,
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,85 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React, { createRef } from 'react';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
function check() {
if (ref.current) return cb(ref.current);
if (Date.now() - start > timeout) return;
setTimeout(check, 30);
}
check();
}
class RefFlowModel extends FlowModel {
ref = createRef<HTMLDivElement>();
render() {
return (
<Card>
<div ref={this.ref} />
</Card>
);
}
}
RefFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {
html: {
type: 'string',
title: 'HTML 内容',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,
},
},
},
async handler(ctx, params) {
waitForRefCallback(ctx.model.ref, (el) => {
el.innerHTML = params.html;
});
},
},
},
});
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ RefFlowModel });
const model = this.flowEngine.createModel({
use: 'RefFlowModel',
stepParams: {
defaultFlow: {
step1: {
html: `<h1>Hello, NocoBase!</h1>
<p>This is a simple HTML content rendered by FlowModel.</p>`,
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
<br />
<FlowsSettings model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,68 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
class HelloFlowModel extends FlowModel {
render() {
const { name } = this.props;
return <Card>Hello {name}</Card>;
}
}
HelloFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {
name: {
type: 'string',
title: 'Name',
'x-component': Input,
},
},
defaultParams: {
name: 'NocoBase',
},
handler(ctx, params) {
console.log('HelloFlowModel step1 handler', params);
ctx.model.setProps('name', params.name);
},
},
},
});
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.registerModels({ HelloFlowModel });
const model = this.flowEngine.createModel({
use: 'HelloFlowModel',
stepParams: {
defaultFlow: {
step1: {
name: 'NocoBase',
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
<br />
<FlowsSettings model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,163 @@
import { faker } from '@faker-js/faker';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, SingleRecordResource } from '@nocobase/flow-engine';
import { Button, message, Space } from 'antd';
import MockAdapter from 'axios-mock-adapter';
import React from 'react';
import { APIClient } from '@nocobase/sdk';
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
const mock = new MockAdapter(api.axios);
const records = [
{
id: 1,
name: faker.person.fullName(),
email: faker.internet.email(),
},
];
mock.onGet('users:get').reply((config) => {
return [
200,
{
data: records[0] || null,
},
];
});
mock.onPost('users:update').reply((config) => {
if (records.length === 0) {
return [
404,
{
errors: [
{
code: 'NotFound',
message: 'Record not found',
},
],
},
];
}
records[0] = {
...records[0],
...JSON.parse(config.data),
};
return [
200,
{
data: records[0],
},
];
});
mock.onPost('users:destroy').reply((config) => {
records.splice(0, 1); // 删除第一个记录
return [
200,
{
data: 1,
},
];
});
// class DataBlockFlowModel extends FlowModel {
// resource;
// constructor(options) {
// super(options);
// this.resource = new SingleRecordResource();
// this.resource.setAPIClient(this.flowEngine.apiClient);
// }
// }
class SingleRecordFlowModel extends FlowModel {
resource = new SingleRecordResource();
render() {
return (
<div>
<div style={{ marginBottom: 16 }}>
<strong>Resource:</strong> {this.resource.getResourceName()} |<strong> FilterByTk:</strong>{' '}
{this.resource.getFilterByTk()}
</div>
<pre>{JSON.stringify(this.resource.getData(), null, 2)}</pre>
<Space>
<Button
onClick={() => {
records[0] = {
id: 1,
name: faker.person.fullName(),
email: faker.internet.email(),
};
this.resource.refresh();
}}
>
Reset
</Button>
<Button onClick={() => this.resource.refresh()}>Refresh</Button>
<Button
onClick={async () =>{
try {
await this.resource.save({
name: faker.person.fullName(),
email: faker.internet.email(),
})
} catch (error) {
message.error(error.message);
}
}}
>
Save
</Button>
<Button onClick={() => this.resource.destroy()} danger>
Delete
</Button>
</Space>
</div>
);
}
}
SingleRecordFlowModel.registerFlow({
auto: true,
key: 'setResourceOptions',
steps: {
step1: {
async handler(ctx, params) {
ctx.model.resource.setAPIClient(api);
ctx.model.resource.setResourceName('users');
ctx.model.resource.setFilterByTk(1);
await ctx.model.resource.refresh();
},
},
},
});
class PluginSingleRecordDemo extends Plugin {
async load() {
this.flowEngine.registerModels({ SingleRecordFlowModel });
const model = this.flowEngine.createModel({
use: 'SingleRecordFlowModel',
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
</div>
),
});
}
}
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginSingleRecordDemo],
});
export default app.getRootComponent();

View File

@ -0,0 +1,149 @@
import { observable } from '@formily/reactive';
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { CreateModelOptions, FlowModel, FlowModelRenderer, IFlowModelRepository } from '@nocobase/flow-engine';
import { Button, Tabs } from 'antd';
import _ from 'lodash';
import React from 'react';
class FlowModelRepository implements IFlowModelRepository<FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }>> {
get models() {
const models = new Map();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('flow-model:')) {
const data = localStorage.getItem(key);
if (data) {
const model = JSON.parse(data);
models.set(model.uid, model);
}
}
}
return models;
}
// 从本地存储加载模型数据
async load(uid: string) {
const data = localStorage.getItem(`flow-model:${uid}`);
if (!data) return null;
const json: FlowModel = JSON.parse(data);
for (const model of this.models.values()) {
if (model.parentId === uid) {
json.subModels = json.subModels || {};
if (model.subType === 'array') {
json.subModels[model.subKey] = json.subModels[model.subKey] || [];
const subModel = await this.load(model.uid);
if (subModel) {
(json.subModels[model.subKey] as FlowModel[]).push(subModel);
}
} else if (model.subType === 'object') {
const subModel = await this.load(model.uid);
json.subModels[model.subKey] = subModel;
}
}
}
return json;
}
// 将模型数据保存到本地存储
async save(model: FlowModel) {
const data = model.serialize();
const currentData = _.omit(data, [...Object.keys(model.subModels)]);
localStorage.setItem(`flow-model:${model.uid}`, JSON.stringify(currentData));
for (const subModelKey of Object.keys(model.subModels)) {
if (!model.subModels[subModelKey]) continue;
if (Array.isArray(model.subModels[subModelKey])) {
model.subModels[subModelKey].forEach((subModel: FlowModel) => {
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()));
}
}
return data;
}
// 从本地存储中删除模型数据
async destroy(uid: string) {
localStorage.removeItem(`flow-model:${uid}`);
return true;
}
}
class TabFlowModel extends FlowModel {}
class HelloFlowModel extends FlowModel<{parent: never, subModels: { tabs: TabFlowModel[] } }> {
addTab(tab: any) {
// 使用新的 addSubModel API 添加子模型
const model = this.addSubModel('tabs', tab);
model.save();
return model;
}
render() {
return (
<div>
<Tabs
items={this.subModels.tabs?.map((tab) => ({
key: tab.getProps().key,
label: tab.getProps().label,
children: tab.render()
}))}
tabBarExtraContent={
<Button
onClick={() => {
const tabId = uid();
this.addTab({
use: 'TabFlowModel',
uid: tabId,
props: { key: tabId, label: `Tab - ${tabId}` },
})
}}
>
Add Tab
</Button>
}
/>
</div>
);
}
}
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.setModelRepository(new FlowModelRepository());
this.flowEngine.registerModels({ HelloFlowModel, TabFlowModel });
const model = await this.flowEngine.loadOrCreateModel({
uid: 'sub-model-test',
use: 'HelloFlowModel',
props: {
name: 'NocoBase',
},
subModels: {
tabs: [
{
use: 'TabFlowModel',
uid: 'tab-1',
props: { key: 'tab-1', label: 'Tab 1' },
},
{
use: 'TabFlowModel',
uid: 'tab-2',
props: { key: 'tab-2', label: 'Tab 2' },
},
],
}
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,222 @@
import { observable } from '@formily/reactive';
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Dropdown, Space, Table, Tabs } from 'antd';
import React from 'react';
async function queryDataSource() {
return [
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖',
age: 42,
address: '西湖区湖底公园1号',
},
];
}
class ActionFlowModel extends FlowModel {
render() {
return <Button {...this.props} />;
}
}
class TableColumnFlowModel extends FlowModel {
render() {
return (value, record, index) => {
return value;
};
}
}
type TableBlockFlowModelStructure = {
subModels: {
columns: TableColumnFlowModel[],
actions: ActionFlowModel[],
}
}
class TableBlockFlowModel extends FlowModel<TableBlockFlowModelStructure> {
addColumn(column) {
return this.addSubModel('columns', column);
}
getColumns() {
return this.subModels.columns
.map((column) => {
return {
...column.getProps(),
render: column.render(),
};
})
.concat({
key: 'addColumn',
fixed: 'right',
title: (
<Dropdown
menu={{
onClick: (info) => {
this.addColumn({
use: 'TableColumnFlowModel',
props: {
title: `新列 ${uid()}`,
dataIndex: `newColumn${this.subModels.columns.length + 1}`,
key: `newColumn${this.subModels.columns.length + 1}`,
},
});
},
items: [
{ key: 'field1', label: 'Field 1' },
{ key: 'field2', label: 'Field 2' },
],
}}
>
<Button>Add column</Button>
</Dropdown>
),
} as any);
}
addAction(action) {
return this.addSubModel('actions', action);
}
renderActions() {
return (
<Space>
{this.subModels.actions.map((action) => {
return <FlowModelRenderer key={action.uid} model={action} />;
})}
<Dropdown
menu={{
onClick: (info) => {
this.addAction({
use: 'ActionFlowModel',
props: {
// type: 'primary',
children: `新动作 ${uid()}`,
// onClick: async () => {},
},
});
},
items: [
{ key: 'add-new', label: 'Add new' },
{ key: 'edit', label: 'Edit' },
],
}}
>
<Button>Add action</Button>
</Dropdown>
</Space>
);
}
render() {
return (
<div>
{this.renderActions()}
<br />
<br />
<Table {...this.props} scroll={{ x: 'max-content' }} columns={this.getColumns()} />
</div>
);
}
}
TableBlockFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {},
async handler(ctx) {
ctx.model.setProps('dataSource', await queryDataSource());
},
},
step2: {
uiSchema: {},
handler(ctx, params) {
const columns = {
name: {
title: '姓名',
dataIndex: 'name',
key: 'name',
},
age: {
title: '年龄',
dataIndex: 'age',
key: 'age',
},
address: {
title: '住址',
dataIndex: 'address',
key: 'address',
},
};
for (const key of params.columns) {
if (!columns[key]) {
throw new Error(`Column ${key} is not defined.`);
}
ctx.model.addColumn({
use: 'TableColumnFlowModel',
props: columns['address'],
});
}
},
},
},
});
// 插件定义
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ TableBlockFlowModel, TableColumnFlowModel, ActionFlowModel });
const model = this.flowEngine.createModel({
use: 'TableBlockFlowModel',
subModels: {
columns: [
{
use: 'TableColumnFlowModel',
props: {
title: '姓名',
dataIndex: 'name',
key: 'name',
},
},
],
actions: [
{
use: 'ActionFlowModel',
props: {
type: 'primary',
children: '查询数据',
},
},
],
},
stepParams: {
defaultFlow: {
step2: {
columns: ['name', 'age'],
},
},
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,221 @@
import { observable } from '@formily/reactive';
import { uid } from '@formily/shared';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Dropdown, Space, Table, Tabs } from 'antd';
import React from 'react';
async function queryDataSource() {
return [
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖',
age: 42,
address: '西湖区湖底公园1号',
},
];
}
class ActionFlowModel extends FlowModel {
render() {
return <Button {...this.props} />;
}
}
class TableColumnFlowModel extends FlowModel {
render() {
return (value, record, index) => {
return value;
};
}
}
type TableBlockFlowModelStructure = {
subModels: {
columns: TableColumnFlowModel[];
actions: ActionFlowModel[];
};
};
class TableBlockFlowModel extends FlowModel<TableBlockFlowModelStructure> {
addColumn(column) {
return this.addSubModel('columns', column);
}
getColumns() {
return this.subModels.columns
.map((column) => {
return {
...column.getProps(),
render: column.render(),
};
})
.concat({
key: 'addColumn',
fixed: 'right',
title: (
<Dropdown
menu={{
onClick: (info) => {
this.addColumn({
use: 'TableColumnFlowModel',
props: {
title: `新列 ${uid()}`,
dataIndex: `newColumn${this.subModels.columns.length + 1}`,
key: `newColumn${this.subModels.columns.length + 1}`,
},
});
},
items: [
{ key: 'field1', label: 'Field 1' },
{ key: 'field2', label: 'Field 2' },
],
}}
>
<Button>Add column</Button>
</Dropdown>
),
} as any);
}
addAction(action) {
return this.addSubModel('actions', action);
}
renderActions() {
return (
<Space>
{this.subModels.actions.map((action) => {
return <FlowModelRenderer key={action.uid} model={action} />;
})}
<Dropdown
menu={{
onClick: (info) => {
this.addAction({
use: 'ActionFlowModel',
props: {
// type: 'primary',
children: `新动作 ${uid()}`,
// onClick: async () => {},
},
});
},
items: [
{ key: 'add-new', label: 'Add new' },
{ key: 'edit', label: 'Edit' },
],
}}
>
<Button>Add action</Button>
</Dropdown>
</Space>
);
}
render() {
return (
<div>
{this.renderActions()}
<br />
<br />
<Table {...this.props} scroll={{ x: 'max-content' }} columns={this.getColumns()} />
</div>
);
}
}
TableBlockFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step1: {
uiSchema: {},
async handler(ctx) {
ctx.model.setProps('dataSource', await queryDataSource());
},
},
step2: {
uiSchema: {},
handler(ctx, params) {
const columns = {
name: {
title: '姓名',
dataIndex: 'name',
key: 'name',
},
age: {
title: '年龄',
dataIndex: 'age',
key: 'age',
},
address: {
title: '住址',
dataIndex: 'address',
key: 'address',
},
};
for (const key of params.columns) {
if (!columns[key]) {
throw new Error(`Column ${key} is not defined.`);
}
ctx.model.addColumn({
use: 'TableColumnFlowModel',
props: columns['address'],
});
}
},
},
},
});
// 插件定义
class PluginTableBlockModel extends Plugin {
async load() {
this.flowEngine.registerModels({ TableBlockFlowModel, TableColumnFlowModel, ActionFlowModel });
const model = this.flowEngine.createModel({
use: 'TableBlockFlowModel',
subModels: {
columns: [
{
use: 'TableColumnFlowModel',
props: {
title: '姓名',
dataIndex: 'name',
key: 'name',
},
},
],
actions: [
{
use: 'ActionFlowModel',
props: {
type: 'primary',
children: '查询数据',
},
},
],
},
stepParams: {
defaultFlow: {
step2: {
columns: ['name', 'age'],
},
},
},
});
this.router.add('root', { path: '/', element: <FlowModelRenderer model={model} /> });
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginTableBlockModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,49 @@
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 () => {},
});
},
},
},
});

View File

@ -0,0 +1,110 @@
import { faker } from '@faker-js/faker';
import MockAdapter from 'axios-mock-adapter';
import { APIClient } from '@nocobase/sdk';
export const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
const mock = new MockAdapter(api.axios);
const records = Array.from({ length: 50 }).map((_, i) => ({
id: i + 1,
username: faker.internet.userName(),
nickname: faker.person.firstName(),
}));
mock.onGet('users:list').reply((config) => {
const page = parseInt(config.params?.page) || 1;
const pageSize = parseInt(config.params?.pageSize) || 3;
const start = (page - 1) * pageSize;
const items = records.slice(start, start + pageSize);
return [
200,
{
data: items,
meta: { page, pageSize, count: records.length },
},
];
});
mock.onPost('users:create').reply((config) => {
const newItem = {
...JSON.parse(config.data),
id: Math.max(...records.map((r) => r.id)) + 1,
};
records.push(newItem);
return [
200,
{
data: newItem,
},
];
});
mock.onGet('users:get').reply((config) => {
const filterByTk = config.params?.filterByTk;
const record = records.find((item) => item.id === filterByTk);
return [
200,
{
data: record,
},
];
});
mock.onPost('users:update').reply((config) => {
console.log('users:update', config);
const filterByTk = config.params?.filterByTk;
const index = records.findIndex((item) => item.id === filterByTk);
if (index === -1) {
return [
404,
{
errors: [
{
code: 'NotFound',
message: 'Record not found',
},
],
},
];
}
records[index] = {
...records[index],
...JSON.parse(config.data),
};
return [
200,
{
data: records[index],
},
];
});
mock.onGet('users:destroy').reply((config) => {
const filterByTk = config.params?.filterByTk;
const index = records.findIndex((item) => item.id === filterByTk);
if (index === -1) {
return [
404,
{
errors: [
{
code: 'NotFound',
message: 'Record not found',
},
],
},
];
}
const deleted = records.splice(index, 1)[0];
return [
200,
{
data: deleted,
},
];
});

View File

@ -0,0 +1,45 @@
import { DataSource, DataSourceManager } from '@nocobase/flow-engine';
export const dsm = new DataSourceManager();
const ds = new DataSource({
name: 'main',
displayName: 'Main',
description: 'This is the main data source',
});
dsm.addDataSource(ds);
ds.addCollection({
name: 'roles',
title: 'Roles',
fields: [
{
name: 'name',
type: 'string',
title: 'Name',
},
{
name: 'uid',
type: 'string',
title: 'UID',
},
],
});
ds.addCollection({
name: 'users',
title: 'Users',
fields: [
{
name: 'username',
type: 'string',
title: 'Username',
},
{
name: 'nickname',
type: 'string',
title: 'Nickname',
},
],
});

View File

@ -0,0 +1,84 @@
import { Plugin } from '@nocobase/client';
import { FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
import { createApp } from '../createApp';
import { FormItemModel } from '../form/form-item-model';
import { FormModel } from '../form/form-model';
import { SubmitActionModel } from '../form/submit-action-model';
import { ActionModel } from './action-model';
import { dsm } from './data-source-manager';
import { TableColumnActionsModel, TableColumnModel } from './table-column-model';
import { TableModel } from './table-model';
class PluginDemo extends Plugin {
async load() {
this.flowEngine.context.dsm = dsm;
this.flowEngine.registerModels({
FormModel,
FormItemModel,
SubmitActionModel,
ActionModel,
TableModel,
TableColumnModel,
TableColumnActionsModel,
});
const model = this.flowEngine.createModel({
use: 'TableModel',
stepParams: {
default: {
step1: {
dataSourceKey: 'main',
collectionName: 'users',
},
},
},
subModels: {
columns: [
{
use: 'TableColumnActionsModel',
subModels: {
actions: [
{
use: 'ActionModel',
stepParams: {
default: {
step1: {
title: 'View',
},
},
},
},
{
use: 'ActionModel',
stepParams: {
default: {
step1: {
title: 'Edit',
},
},
},
},
],
},
},
{
use: 'TableColumnModel',
stepParams: {
default: {
step1: {
fieldPath: 'main.users.username',
},
},
},
},
],
},
});
this.router.add('root', {
path: '/',
element: <FlowModelRenderer model={model} />,
});
}
}
export default createApp({ plugins: [PluginDemo] });

View File

@ -0,0 +1,106 @@
import { EditOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Space } from 'antd';
import React from 'react';
import { FormModel } from '../form/form-model';
import { ActionModel } from './action-model';
export class TableColumnModel extends FlowModel {
field: Field;
fieldPath: string;
getColumnProps() {
return { ...this.props, render: this.render() };
}
render() {
return (value, record, index) => (
<span
className={css`
.anticon {
display: none;
}
&:hover {
.anticon {
display: inline-flex;
}
}
`}
>
{value}
<EditOutlined
onClick={async () => {
const model = this.createRootModel({
use: 'FormModel',
stepParams: {
default: {
step1: {
dataSourceKey: 'main',
collectionName: 'users',
},
},
},
subModels: {
fields: [
{
use: 'FormItemModel',
stepParams: {
default: {
step1: {
fieldPath: this.fieldPath,
},
},
},
},
],
},
}) as FormModel;
await model.openDialog({ filterByTk: record.id });
await this.parent.resource.refresh();
this.flowEngine.removeModel(model.uid);
}}
/>
</span>
);
}
}
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.dsm.getCollectionField(params.fieldPath);
ctx.model.fieldPath = params.fieldPath;
ctx.model.setProps('title', field.title);
ctx.model.setProps('dataIndex', field.name);
ctx.model.field = field;
},
},
},
});

View File

@ -0,0 +1,94 @@
import { Collection, FlowModel, MultiRecordResource } from '@nocobase/flow-engine';
import { Button, Dropdown, Table } from 'antd';
import React from 'react';
import { api } from './api';
import { TableColumnModel } from './table-column-model';
type S = {
subModels: {
columns: TableColumnModel[];
};
};
export class TableModel extends FlowModel<S> {
collection: Collection;
resource: MultiRecordResource;
getColumns() {
return this.mapSubModels('columns', (column) => column.getColumnProps()).concat({
key: 'addColumn',
fixed: 'right',
title: (
<Dropdown
menu={{
onClick: (info) => {
const model = this.addSubModel('columns', {
use: 'TableColumnModel',
stepParams: {
default: {
step1: {
fieldPath: info.key,
},
},
},
});
model.applyAutoFlows();
},
items: this.collection.mapFields((field) => {
return {
key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`,
label: field.title,
};
}),
}}
>
<Button>Add column</Button>
</Dropdown>
),
} as any);
}
render() {
return (
<div>
<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();
}}
/>
</div>
);
}
}
TableModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
async handler(ctx, params) {
if (ctx.model.collection) {
return;
}
ctx.model.collection = ctx.globals.dsm.getCollection(params.dataSourceKey, params.collectionName);
const resource = new MultiRecordResource();
resource.setDataSourceKey(params.dataSourceKey);
resource.setResourceName(params.collectionName);
resource.setAPIClient(api);
ctx.model.resource = resource;
await resource.refresh();
await ctx.model.applySubModelsAutoFlows('columns');
},
},
},
});

View File

@ -0,0 +1,122 @@
import { Input } from '@formily/antd-v5';
import { Application, Plugin } from '@nocobase/client';
import { FlowModel, FlowModelRenderer, FlowsSettings } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React, { createRef } from 'react';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
function check() {
if (ref.current) return cb(ref.current);
if (Date.now() - start > timeout) return;
setTimeout(check, 30);
}
check();
}
class RefFlowModel extends FlowModel {
ref = createRef<HTMLDivElement>();
render() {
return (
<Card>
<div ref={this.ref} style={{ width: '100%', height: 400 }} />
</Card>
);
}
}
RefFlowModel.registerFlow('defaultFlow', {
auto: true,
steps: {
step0: {
use: 'require',
defaultParams: {
paths: {
requireEcharts2: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min',
},
},
},
step1: {
uiSchema: {
option: {
type: 'string',
title: 'ECharts 配置',
'x-component': Input.TextArea,
},
},
async handler(ctx, params) {
waitForRefCallback(ctx.model.ref, async (el) => {
const echarts = await ctx.globals.requireAsync('requireEcharts2');
const chart = echarts.init(el);
chart.setOption(JSON.parse(params.option));
});
},
},
},
});
// 插件定义
class PluginHelloModel extends Plugin {
async load() {
this.flowEngine.setContext({
requireAsync: async (mod) => {
return new Promise((resolve, reject) => {
this.app.requirejs.require([mod], (arg) => resolve(arg), reject);
});
},
});
this.flowEngine.registerAction('require', {
handler: (ctx, params) => {
this.app.requirejs.require.config({
// @ts-ignore
paths: params.paths,
});
},
});
this.flowEngine.registerModels({ RefFlowModel });
const model = this.flowEngine.createModel({
use: 'RefFlowModel',
stepParams: {
defaultFlow: {
step1: {
option: JSON.stringify({
title: {
text: 'ECharts 示例',
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
}),
},
},
},
});
this.router.add('root', {
path: '/',
element: (
<div>
<FlowModelRenderer model={model} />
<br />
<FlowsSettings model={model} />
</div>
),
});
}
}
// 创建应用实例
const app = new Application({
router: { type: 'memory', initialEntries: ['/'] },
plugins: [PluginHelloModel],
});
export default app.getRootComponent();

View File

@ -0,0 +1,5 @@
# FormFlowModel
## 演示
<code src="./demos/form/index.tsx"></code>

View File

@ -0,0 +1,86 @@
# Flow Models
## Basic
<code src="./demos/hello.tsx"></code>
## Set props
<code src="./demos/hello-set-props.tsx"></code>
## onInit
<code src="./demos/on-init.tsx"></code>
## sub-model
<code src="./demos/sub-model.tsx"></code>
## register flow
<code src="./demos/register-flow.tsx"></code>
## table block
<code src="./demos/table-block.tsx"></code>
## react-big-calendar
<code src="./demos/react-big-calendar.tsx"></code>
## formily
<code src="./demos/formily.tsx"></code>
## ref
<code src="./demos/ref.tsx"></code>
## grid
<code src="./demos/grid.tsx"></code>
## FlowsFloatContextMenu
<code src="./demos/FlowsFloatContextMenu.tsx"></code>
## FlowsContextMenu
<code src="./demos/FlowsContextMenu.tsx"></code>
## 事件触发
<code src="./demos/dispatch-event.tsx"></code>
## 数据源
<code src="./demos/data-source.tsx"></code>
## collection inherits
<code src="./demos/collection-inherits.tsx"></code>
## configure fields
<code src="./demos/configure-fields.tsx"></code>
## Array Resource
<code src="./demos/array-resource.tsx"></code>
## Object Resource
<code src="./demos/object-resource.tsx"></code>
## Single Record Resource
<code src="./demos/single-record-resource.tsx"></code>
## Multi Record Resource
<code src="./demos/multi-record-resource.tsx"></code>
## 必填参数配置对话框
<code src="./demos/open-required-step-params-dialog.tsx"></code>

View File

@ -0,0 +1,5 @@
# LayoutFlowModel
## 演示
<code src="./demos/layout-flow-model.tsx"></code>

View File

@ -0,0 +1 @@
# LayoutRouteFlowModel

View File

@ -0,0 +1 @@
# PageFlowModel

View File

@ -0,0 +1 @@
# PageTabFlowModel

View File

@ -0,0 +1,269 @@
# 快速开始:用 FlowModel 构建可编排的按钮组件
在 React 中,我们通常这样渲染一个按钮组件:
```tsx | pure
import { Button } from 'antd';
export default function App() {
return <Button type="primary">Primary Button</Button>;
}
```
上述代码虽然简单,但属于**静态组件**,无法满足无代码平台对可配置性和编排能力的需求。
在 NocoBase 的 FlowEngine 中,我们可以通过 **FlowModel + FlowDefinition** 快速构建支持配置和事件驱动的组件,实现更强大的无代码能力。
---
## 第一步:使用 FlowModel 渲染组件
<code src="./demos/quickstart-1-basic.tsx"></code>
### 🧠 关键概念
- `FlowModel` 是 FlowEngine 中的核心组件模型,封装组件逻辑、渲染和配置能力。
- 每个 UI 组件都可以通过 `FlowModel` 进行实例化并统一管理。
### 📌 实现步骤
#### 1. 创建自定义模型类
```tsx | pure
class MyModel extends FlowModel {
render() {
return <Button {...this.props} />;
}
}
```
#### 2. 创建 model 实例
```ts
const model = this.flowEngine.createModel({
uid: 'my-model',
use: 'MyModel',
props: {
type: 'primary',
children: 'Primary Button',
},
});
```
#### 3. 使用 `<FlowModelRenderer />` 渲染
```tsx | pure
<FlowModelRenderer model={model} />
```
---
## 第二步:添加 PropsFlow使按钮属性可配置
<code src="./demos/quickstart-2-register-propsflow.tsx"></code>
### 💡 为什么要用 PropsFlow
使用 Flow 而非静态 props可以实现属性的
- 动态配置
- 可视化编辑
- 状态回放与持久化
### 🛠 关键改造点
#### 1. 定义按钮属性的 Flow
```tsx | pure
const myPropsFlow = defineFlow({
key: 'myPropsFlow',
auto: true,
title: '按钮配置',
steps: {
setProps: {
title: '按钮属性设置',
uiSchema: {
title: {
type: 'string',
title: '按钮标题',
'x-component': 'Input',
},
type: {
type: 'string',
title: '类型',
'x-component': 'Select',
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',
enum: [
{ label: '搜索', value: 'SearchOutlined' },
{ label: '添加', value: 'PlusOutlined' },
{ label: '删除', value: 'DeleteOutlined' },
{ label: '编辑', value: 'EditOutlined' },
{ label: '设置', value: 'SettingOutlined' },
],
},
},
defaultParams: {
type: 'primary',
},
handler(ctx, params) {
ctx.model.setProps('children', params.title);
ctx.model.setProps('type', params.type);
const icon = params.icon ? React.createElement(icons[params.icon]) : undefined;
ctx.model.setProps('icon', icon);
},
},
},
});
MyModel.registerFlow(myPropsFlow);
```
#### 2. 使用 `stepParams` 替代静态 `props`
```diff
const model = this.flowEngine.createModel({
uid: 'my-model',
use: 'MyModel',
- props: {
- type: 'primary',
- children: 'Primary Button',
- },
+ stepParams: {
+ myPropsFlow: {
+ setProps: {
+ title: 'Primary Button',
+ type: 'primary',
+ },
+ },
+ },
});
```
> ✅ 使用 `stepParams` 是 FlowEngine 推荐方式,可避免不可序列化数据(如 React 组件)的问题。
#### 3. 启用属性配置界面
```diff
- <FlowModelRenderer model={model} />
+ <FlowModelRenderer model={model} showFlowSettings />
```
---
## 第三步支持按钮事件流EventFlow
<code src="./demos/quickstart-3-register-eventflow.tsx"></code>
### 🎯 场景:点击按钮后弹出确认框
#### 1. 在模型中派发事件
```tsx | pure
class MyModel extends FlowModel {
render() {
return (
<Button
{...this.props}
onClick={(event) => {
this.dispatchEvent('onClick', { event });
}}
/>
);
}
}
```
#### 2. 定义事件流
```ts
const myEventFlow = defineFlow({
key: 'myEventFlow',
on: {
eventName: 'onClick',
},
title: '按钮事件',
steps: {
confirm: {
title: '确认操作配置',
uiSchema: {
title: {
type: 'string',
title: '弹窗提示标题',
'x-component': 'Input',
},
content: {
type: 'string',
title: '弹窗提示内容',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
title: '确认操作',
content: '你点击了按钮,是否确认?',
},
handler(ctx, params) {
Modal.confirm({
...params,
});
},
},
},
});
```
---
## 模型对比图ReactComponent vs FlowModel
Flow 并不会改变组件的实现方式。它只是为 ReactComponent 增加了对 PropsFlow 和 EventFlow 的支持,从而让组件的属性和事件都可以通过可视化方式配置和编排。
<img style="width: 500px;" src="https://static-docs.nocobase.com/20250603132845.png">
### ReactComponent
```mermaid
graph TD
Button[ButtonComponent]
Button --> Props[Props]
Button --> Events[Events]
Props --> title[title]
Props --> type[type]
Props --> icon[icon]
Events --> onClick[onClick]
```
### FlowModel
```mermaid
graph TD
Button[ButtonModel]
Button --> Props[PropsFlow]
Button --> Events[EventFlow]
Props --> title[title]
Props --> type[type]
Props --> icon[icon]
Events --> onClick[onClick]
```
---
## 总结
通过以上三步,我们完成了一个支持配置与事件编排的按钮组件,具备以下优势:
- 🚀 可视化配置属性(如标题、类型、图标)
- 🔄 事件响应可被流程接管(如点击弹窗)
- 🔧 支持后续拓展(如条件逻辑、变量绑定等)
这种模式也适用于表单、列表、图表等任何 UI 组件,在 NocoBase 的 FlowEngine 中,**一切皆可编排**。

View File

@ -0,0 +1,5 @@
# TableFlowModel
## 演示
<code src="./demos/table/index.tsx"></code>

View File

@ -40,6 +40,7 @@ import { DataSourceApplicationProvider } from '../data-source/components/DataSou
import { DataBlockProvider } from '../data-source/data-block/DataBlockProvider';
import { DataSourceManager, type DataSourceManagerOptions } from '../data-source/data-source/DataSourceManager';
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
import type { CollectionFieldInterfaceFactory } from '../data-source';
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
@ -131,6 +132,7 @@ export class Application {
public globalVars: Record<string, any> = {};
public globalVarCtxs: Record<string, any> = {};
public jsonLogic: JsonLogic;
public flowEngine: FlowEngine;
loading = true;
maintained = false;
maintaining = false;
@ -191,6 +193,7 @@ export class Application {
this.pluginManager = new PluginManager(options.plugins, options.loadRemotePlugins, this);
this.schemaInitializerManager = new SchemaInitializerManager(options.schemaInitializers, this);
this.dataSourceManager = new DataSourceManager(options.dataSourceManager, this);
this.flowEngine = new FlowEngine();
this.addDefaultProviders();
this.addReactRouterComponents();
this.addProviders(options.providers || []);
@ -270,6 +273,8 @@ export class Application {
this.use(AntdAppProvider);
this.use(DataSourceApplicationProvider, { dataSourceManager: this.dataSourceManager });
this.use(OpenModeProvider);
this.flowEngine.context['app'] = this;
this.use(FlowEngineProvider, { engine: this.flowEngine });
}
private addReactRouterComponents() {
@ -360,6 +365,7 @@ export class Application {
this.loading = true;
await this.loadWebSocket();
await this.pm.load();
await this.flowEngine.flowSettings.load();
} catch (error) {
this.hasLoadError = true;
@ -548,6 +554,7 @@ export class Application {
getGlobalVarCtx(key) {
return get(this.globalVarCtxs, key);
}
addUserCenterSettingsItem(item: SchemaSettingsItemType & { aclSnippet?: string }) {
const useVisibleProp = item.useVisible || (() => true);
const useVisible = () => {

View File

@ -23,6 +23,10 @@ export class Plugin<T = any> {
return this.app.pluginManager;
}
get flowEngine() {
return this.app.flowEngine;
}
get pm() {
return this.app.pm;
}

View File

@ -11,7 +11,7 @@
// @ts-nocheck
/* eslint-disable */
/* prettier-ignore */
import type { Require, RequireDefine } from './types'
import type { Require, RequireDefine } from './types';
export interface RequireJS {
require: Require

View File

@ -0,0 +1,78 @@
/**
* 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 React, { createContext, useContext, ReactNode, useRef, useCallback } from "react";
export const BlockConfigsContext = createContext<{
getConfigs: () => any;
setConfigs: (value: any, shouldNotify?: boolean) => void;
getFiltersConfigs: () => any;
getEventsConfigs: () => any;
subscribe: (callback: () => void) => () => void;
}>({
getConfigs: () => null,
setConfigs: () => {},
getFiltersConfigs: () => ({}),
getEventsConfigs: () => ({}),
subscribe: () => () => {},
});
// 提供一个自定义 Hook 用于访问 BlockConfigs 上下文
export const useBlockConfigs = () => {
return useContext(BlockConfigsContext);
};
export const BlockConfigsProvider = ({ children, value: initialValue }: { children: ReactNode; value: any }) => {
const valueRef = useRef(initialValue);
const subscribersRef = useRef<Array<() => void>>([]);
const getConfigs = useCallback(() => {
return valueRef.current;
}, []);
const setConfigs = useCallback((newValue: any, shouldNotify = false) => {
valueRef.current = newValue;
// 仅在明确要求通知时执行回调
if (shouldNotify) {
subscribersRef.current.forEach(callback => callback());
}
}, []);
const getFiltersConfigs = useCallback(() => {
return valueRef.current?.configData?.filterSteps || {};
}, []);
const getEventsConfigs = useCallback(() => {
return valueRef.current?.configData?.events || {};
}, []);
const subscribe = useCallback((callback: () => void) => {
subscribersRef.current.push(callback);
// 返回取消订阅函数
return () => {
subscribersRef.current = subscribersRef.current.filter(cb => cb !== callback);
};
}, []);
const contextValue = useRef({
getConfigs,
setConfigs,
getFiltersConfigs,
getEventsConfigs,
subscribe
}).current;
return (
<BlockConfigsContext.Provider value={contextValue}>
{children}
</BlockConfigsContext.Provider>
);
};

View File

@ -0,0 +1,10 @@
/**
* 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 './BlockConfigsProvider';

View File

@ -0,0 +1,14 @@
/**
* 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 React from 'react';
export const FlowEngineRunner = (props) => {
return <>{props.children}</>;
};

View File

@ -0,0 +1,79 @@
/**
* 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, IFlowModelRepository } from '@nocobase/flow-engine';
import _ from 'lodash';
export class MockFlowModelRepository implements IFlowModelRepository<FlowModel> {
get models() {
const models = new Map();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('flow-model:')) {
const data = localStorage.getItem(key);
if (data) {
const model = JSON.parse(data);
models.set(model.uid, model);
}
}
}
return models;
}
// 从本地存储加载模型数据
async load(uid: string) {
const data = localStorage.getItem(`flow-model:${uid}`);
if (!data) {
console.warn(`Model with uid ${uid} not found in localStorage.`);
return null;
}
const json = JSON.parse(data);
for (const model of [...this.models.values()]) {
if (model.parentId === uid) {
json.subModels = json.subModels || {};
if (model.subType === 'array') {
json.subModels[model.subKey] = json.subModels[model.subKey] || [];
const subModel = await this.load(model.uid);
json.subModels[model.subKey].push(subModel);
} else if (model.subType === 'object') {
const subModel = await this.load(model.uid);
json.subModels[model.subKey] = subModel;
}
}
}
console.log('Loading model:', uid, JSON.stringify(json, null, 2));
return json;
}
// 将模型数据保存到本地存储
async save(model: FlowModel) {
const data = model.serialize();
const currentData = _.omit(data, ['subModels', 'flowEngine']);
localStorage.setItem(`flow-model:${model.uid}`, JSON.stringify(currentData));
console.log('Saving model:', model.uid, currentData);
for (const subModelKey of Object.keys(model.subModels || {})) {
const subModelValue = model.subModels[subModelKey];
if (!subModelValue) continue;
if (Array.isArray(subModelValue)) {
for (const subModel of subModelValue) {
await this.save(subModel);
}
} else if (subModelValue instanceof FlowModel) {
await this.save(subModelValue);
}
}
return data;
}
// 从本地存储中删除模型数据
async destroy(uid: string) {
localStorage.removeItem(`flow-model:${uid}`);
return true;
}
}

View File

@ -0,0 +1,55 @@
/**
* 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 { FlowModelRenderer, useFlowEngine, useFlowModel } from '@nocobase/flow-engine';
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import React from 'react';
import { useParams } from 'react-router-dom';
function InternalFlowPage({ uid }) {
const model = useFlowModel(uid);
return <FlowModelRenderer model={model} showFlowSettings hideRemoveInSettings/>;
}
export const FlowPage = () => {
const params = useParams();
return <FlowPageComponent uid={params.name} />;
};
export const FlowPageComponent = ({ uid }) => {
const flowEngine = useFlowEngine();
const { loading } = useRequest(
() => {
return flowEngine.loadOrCreateModel({
uid: uid,
use: 'PageFlowModel',
subModels: {
tabs: [
{
use: 'PageTabFlowModel',
subModels: {
grid: {
use: 'BlockGridFlowModel',
},
},
},
],
},
});
},
{
refreshDeps: [uid],
},
);
if (loading) {
return <Spin />;
}
return <InternalFlowPage uid={uid} />;
};

View File

@ -0,0 +1,55 @@
/**
* 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 { DataSource, DataSourceManager, FlowModel } from '@nocobase/flow-engine';
import _ from 'lodash';
import { Plugin } from '../application/Plugin';
import { FlowEngineRunner } from './FlowEngineRunner';
import { MockFlowModelRepository } from './FlowModelRepository';
import { FlowPage } from './FlowPage';
import * as models from './models';
export class PluginFlowEngine extends Plugin {
async load() {
this.app.addComponents({ FlowPage });
this.app.flowEngine.setModelRepository(new MockFlowModelRepository());
const filteredModels = Object.fromEntries(
Object.entries(models).filter(
([, ModelClass]) => typeof ModelClass === 'function' && ModelClass.prototype instanceof FlowModel,
),
);
console.log('Registering flow models:', Object.keys(filteredModels));
this.flowEngine.registerModels(filteredModels);
const dataSourceManager = new DataSourceManager();
this.flowEngine.context['app'] = this.app;
this.flowEngine.context['api'] = this.app.apiClient;
this.flowEngine.context['dataSourceManager'] = dataSourceManager;
try {
const response = await this.app.apiClient.request<any>({
url: '/collections:listMeta',
});
const mainDataSource = new DataSource({
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, {});
}
}
// Export all models for external use
export * from './models';

View 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.
*/
import { DefaultStructure, FlowModel } from '@nocobase/flow-engine';
export class BlockFlowModel<T = DefaultStructure> extends FlowModel<T> {}

View File

@ -0,0 +1,80 @@
/**
* 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 { AddBlockButton, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Card, Dropdown } from 'antd';
import _ from 'lodash';
import React from 'react';
import { BlockFlowModel } from './BlockFlowModel';
function Grid({ items }) {
return (
<div>
{items.map((item) => {
return (
<div key={item.uid} style={{ marginBottom: 16 }}>
<FlowModelRenderer model={item} key={item.uid} showFlowSettings />
</div>
);
})}
</div>
);
}
// function AddBlockButton({ model }) {
// return (
// <Dropdown
// menu={{
// onClick: (info) => {
// const BlockModel = model.flowEngine.getModelClass(info.key);
// model.addItem(_.cloneDeep(BlockModel.meta.defaultOptions));
// },
// items: model.getBlockModels(),
// }}
// >
// <Button>Add block</Button>
// </Dropdown>
// );
// }
type BlockGridFlowModelStructure = {
subModels: {
items: BlockFlowModel[];
};
};
export class BlockGridFlowModel extends FlowModel<BlockGridFlowModelStructure> {
// addItem(item) {
// const model = this.addSubModel('items', item);
// model.save();
// }
// getBlockModels() {
// return [...this.flowEngine.getModelClasses()]
// .filter(([, Model]) => {
// return Model.prototype instanceof BlockFlowModel;
// })
// .map(([key, Model]) => {
// const meta = (Model as typeof BlockFlowModel).meta;
// return {
// key,
// label: meta.title,
// };
// });
// }
render() {
return (
<div style={{ padding: 16 }}>
<Grid items={this.subModels.items?.slice() || []} />
<AddBlockButton model={this} subModelKey="items" />
</div>
);
}
}

View File

@ -0,0 +1,188 @@
/**
* 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 { Application, Plugin } from '@nocobase/client';
import { Collection, FlowModel, MultiRecordResource } from '@nocobase/flow-engine';
import { Card, Modal } from 'antd';
import moment from 'moment';
import dataSource from 'packages/core/client/docs/zh-CN/core/flow-models/demos/data-source';
import { createdAt } from 'packages/plugins/@nocobase/plugin-mock-collections/src/server/field-interfaces';
import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { BlockFlowModel } from './BlockFlowModel';
const localizer = momentLocalizer(moment);
export class CalendarBlockFlowModel extends BlockFlowModel {
collection: Collection;
resource: MultiRecordResource;
render() {
const data = this.resource.getData();
return (
<Card>
<Calendar
localizer={localizer}
style={{ height: 500 }}
{...this.props}
onSelectEvent={(event) => {
this.dispatchEvent('onSelectEvent', { event });
}}
onDoubleClickEvent={(event) => {
this.dispatchEvent('onDoubleClickEvent', { event });
}}
events={data.map((item) => {
return {
...item,
createdAt: item.createdAt ? moment(item.createdAt).toDate() : undefined,
};
})}
/>
</Card>
);
}
}
CalendarBlockFlowModel.registerFlow({
key: 'key2',
on: {
eventName: 'onSelectEvent',
},
steps: {
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
Modal.info({
title: 'Event Selected',
content: (
<div>
<p>Title: {ctx.extra.event.nickname}</p>
<p>Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
</div>
),
});
},
},
},
});
CalendarBlockFlowModel.registerFlow({
key: 'key3',
on: {
eventName: 'onDoubleClickEvent',
},
steps: {
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
Modal.info({
title: 'Double Click',
content: (
<div>
<p>Title: {ctx.extra.event.nickname}</p>
<p>Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
</div>
),
});
},
},
},
});
CalendarBlockFlowModel.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();
},
},
step2: {
paramsRequired: true,
uiSchema: {
titleAccessor: {
type: 'string',
title: 'Title accessor',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter title accessor',
},
},
startAccessor: {
type: 'string',
title: 'Start accessor',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter start accessor',
},
},
endAccessor: {
type: 'string',
title: 'End accessor',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter end accessor',
},
},
},
handler: async (ctx, params) => {
console.log('CalendarBlockFlowModel step2 params:', params);
ctx.model.setProps('titleAccessor', params.titleAccessor);
ctx.model.setProps('startAccessor', params.startAccessor);
ctx.model.setProps('endAccessor', params.endAccessor);
},
},
},
});
CalendarBlockFlowModel.define({
title: 'Calendar',
group: 'Content',
defaultOptions: {
use: 'CalendarBlockFlowModel',
},
});

View File

@ -0,0 +1,6 @@
import { Field, FlowModel } from '@nocobase/flow-engine';
export class FieldFlowModel extends FlowModel {
field: Field;
fieldPath: string;
}

View File

@ -0,0 +1,75 @@
/**
* 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 { Card } from 'antd';
import React, { createRef } from 'react';
import { BlockFlowModel } from './BlockFlowModel';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
const start = Date.now();
function check() {
if (ref.current) return cb(ref.current);
if (Date.now() - start > timeout) return;
setTimeout(check, 30);
}
check();
}
export class HtmlBlockFlowModel extends BlockFlowModel {
ref = createRef<HTMLDivElement>();
render() {
return (
<Card>
<div ref={this.ref} />
{/* <div dangerouslySetInnerHTML={{ __html: this.props.html }} /> */}
</Card>
);
}
}
HtmlBlockFlowModel.define({
title: 'HTML',
group: 'Content',
defaultOptions: {
use: 'HtmlBlockFlowModel',
stepParams: {
default: {
step1: {
html: `<h1>Hello, NocoBase!</h1>
<p>This is a simple HTML content rendered by FlowModel.</p>`,
},
},
},
},
});
HtmlBlockFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
uiSchema: {
html: {
type: 'string',
title: 'HTML 内容',
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,
},
},
},
async handler(ctx, params) {
waitForRefCallback(ctx.model.ref, (el) => {
el.innerHTML = params.html;
});
// ctx.model.setProps('html', params.html);
},
},
},
});

View File

@ -0,0 +1,99 @@
/**
* 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 { uid } from '@formily/shared';
import { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Button, Tabs } from 'antd';
import _ from 'lodash';
import React from 'react';
type PageFlowModelStructure = {
subModels: {
tabs: FlowModel[];
};
};
export class PageFlowModel extends FlowModel<PageFlowModelStructure> {
addTab(tab: any) {
const model = this.addSubModel('tabs', tab);
model.save();
}
getItems() {
return this.subModels.tabs?.map((tab) => {
return {
key: tab.uid,
label: tab.props.label || 'Unnamed',
children: <FlowModelRenderer model={tab} />,
};
});
}
renderFirstTab() {
return <FlowModelRenderer model={this.subModels.tabs?.[0]} />;
}
renderTabs() {
return (
<Tabs
items={this.getItems()}
// destroyInactiveTabPane
tabBarExtraContent={
<Button
onClick={() =>
this.addTab({
use: 'PageTabFlowModel',
props: { key: uid(), label: `Tab - ${uid()}` },
grid: {
use: 'BlockGridFlowModel',
},
})
}
>
Add Tab
</Button>
}
/>
);
}
render() {
return this.props.enableTabs ? this.renderTabs() : this.renderFirstTab();
}
}
PageFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
uiSchema: {
title: {
type: 'string',
title: 'Page Title',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter page title',
},
},
enableTabs: {
type: 'boolean',
title: 'Enable tabs',
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
},
async handler(ctx, params) {
ctx.model.setProps('enableTabs', params.enableTabs || false);
console.log('PageFlowModel step1 handler', ctx.model.props.enableTabs);
},
},
},
});

View File

@ -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 { FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import React from 'react';
import { BlockGridFlowModel } from './BlockGridFlowModel';
export class PageTabFlowModel extends FlowModel<{
subModels: {
grid: BlockGridFlowModel;
};
}> {
render() {
console.log('TabFlowModel render', this.uid);
return (
<div>
<FlowModelRenderer model={this.subModels.grid} />
</div>
);
}
}
PageTabFlowModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
async handler(ctx, params) {
// model.setProps('label', `Tab123 - ${model.uid}`);
// model.setProps('children', model.renderChildren());
},
},
},
});

View File

@ -0,0 +1,58 @@
/**
* 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 () => {},
});
},
},
},
});

View File

@ -0,0 +1,51 @@
/**
* 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 as FormilyField } from '@formily/react';
import { Field, FlowModel } from '@nocobase/flow-engine';
import React from 'react';
export class FormItemModel extends FlowModel {
field: Field;
render() {
return (
<div>
<FormilyField
name={this.field.name}
title={this.field.title}
required
decorator={[FormItem]}
component={[
Input,
{
style: {
width: 240,
},
},
]}
/>
</div>
);
}
}
FormItemModel.registerFlow({
key: 'default',
auto: true,
steps: {
step1: {
handler(ctx, params) {
const field = ctx.globals.dataSourceManager.getCollectionField(params.fieldPath);
ctx.model.field = field;
},
},
},
});

View File

@ -0,0 +1,141 @@
/**
* 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, FormItem, Input, Submit } from '@formily/antd-v5';
import { createForm, Form } from '@formily/core';
import { FormProvider } from '@formily/react';
import {
Collection,
FlowEngineProvider,
FlowModel,
FlowModelRenderer,
SingleRecordResource,
} from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
import { BlockFlowModel } from './BlockFlowModel';
export class FormModel extends BlockFlowModel {
form: Form;
resource: SingleRecordResource;
collection: Collection;
render() {
return (
<div>
<FormProvider form={this.form}>
{this.mapSubModels('fields', (field) => (
<FlowModelRenderer model={field} />
))}
<FormButtonGroup>
{this.mapSubModels('actions', (action) => (
<FlowModelRenderer model={action} />
))}
</FormButtonGroup>
<br />
<Card>
<pre>{JSON.stringify(this.form.values, null, 2)}</pre>
</Card>
</FormProvider>
</div>
);
}
async openDialog({ filterByTk }) {
return new Promise((resolve) => {
const dialog = FormDialog(
{
footer: null,
title: 'Form Dialog',
},
(form) => {
return (
<div>
<FlowEngineProvider engine={this.flowEngine}>
<FlowModelRenderer model={this} extraContext={{ form, filterByTk }} />
<FormButtonGroup>
<Submit
onClick={async () => {
await this.resource.save(this.form.values);
dialog.close();
resolve(this.form.values); // 在 close 之后 resolve
}}
>
Submit
</Submit>
</FormButtonGroup>
</FlowEngineProvider>
</div>
);
},
);
dialog.open();
// 可选:如果需要在取消时也 resolve可以监听 dialog 的 onCancel
});
}
}
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',
},
});

View File

@ -0,0 +1,22 @@
/**
* 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 './action-model';
export * from './BlockFlowModel';
export * from './BlockGridFlowModel';
export * from './CalendarBlockFlowModel';
export * from './form-item-model';
export * from './form-model';
export * from './HtmlBlockFlowModel';
export * from './PageFlowModel';
export * from './PageTabFlowModel';
export * from './submit-action-model';
export * from './table-column-model';
export * from './table-model';
//

View File

@ -0,0 +1,37 @@
/**
* 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 './action-model';
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();
}
},
},
},
});

View File

@ -0,0 +1,125 @@
/**
* 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 { Field, FlowModel, FlowModelRenderer } from '@nocobase/flow-engine';
import { Space } from 'antd';
import React from 'react';
import { ActionModel } from './action-model';
import { FormModel } from './form-model';
import { FieldFlowModel } from './FieldFlowModel';
export class TableColumnModel extends FieldFlowModel {
// field: Field;
// fieldPath: string;
getColumnProps() {
return { ...this.props, render: this.render() };
}
render() {
return (value, record, index) => (
<span
className={css`
.anticon {
display: none;
}
&:hover {
.anticon {
display: inline-flex;
}
}
`}
>
{value}
<EditOutlined
onClick={async () => {
const model = this.createRootModel({
use: 'FormModel',
stepParams: {
default: {
step1: {
dataSourceKey: this.field.collection.dataSource.name,
collectionName: this.field.collection.name,
},
},
},
subModels: {
fields: [
{
use: 'FormItemModel',
stepParams: {
default: {
step1: {
fieldPath: this.fieldPath,
},
},
},
},
],
},
}) as FormModel;
await model.openDialog({ filterByTk: record.id });
await this.parent.resource.refresh();
this.flowEngine.removeModel(model.uid);
}}
/>
</span>
);
}
}
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;
},
},
},
});

View File

@ -0,0 +1,112 @@
/**
* 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 { Collection, FlowModel, MultiRecordResource } from '@nocobase/flow-engine';
import { Button, Card, Dropdown, Table } from 'antd';
import React from 'react';
import { BlockFlowModel } from './BlockFlowModel';
import { TableColumnModel } from './table-column-model';
import { AddFieldButton } from '@nocobase/flow-engine';
import { FieldFlowModel } from './FieldFlowModel';
type S = {
subModels: {
columns: TableColumnModel[];
};
};
export class TableModel extends BlockFlowModel<S> {
collection: Collection;
resource: MultiRecordResource;
getColumns() {
return this.mapSubModels('columns', (column) => column.getColumnProps()).concat({
key: 'addColumn',
fixed: 'right',
title: (
<AddFieldButton 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',
},
});

View File

@ -28,6 +28,7 @@ export * from './api-client';
export * from './appInfo';
export * from './application';
export * from './async-data-provider';
export * from './block-configs';
export * from './block-provider';
export * from './collection-manager';
@ -37,6 +38,7 @@ export * from './data-source';
export * from './document-title';
export * from './filter-provider';
export * from './flag-provider';
export * from './flow';
export * from './global-theme';
export * from './hooks';
export * from './i18n';

View File

@ -0,0 +1,121 @@
/**
* 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 { FormLayout } from '@formily/antd-v5';
import { SchemaOptionsContext } from '@formily/react';
import { uid } from '@formily/shared';
import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../api-client/hooks/useAPIClient';
import { SchemaInitializerItem } from '../../application';
import { useGlobalTheme } from '../../global-theme';
import { NocoBaseDesktopRouteType } from '../../route-switch/antd/admin-layout/convertRoutesToSchema';
import {
FormDialog,
SchemaComponent,
SchemaComponentOptions,
useNocoBaseRoutes,
useParentRoute,
} from '../../schema-component';
import { useStyles } from '../../schema-component/antd/menu/MenuItemInitializers';
const useInsertFlowPageSchema = () => {
const api = useAPIClient();
return useCallback(
async (schema) => {
await api.request({
method: 'POST',
url: '/uiSchemas:insert',
data: schema,
});
},
[api],
);
};
export const FlowPageMenuItem = () => {
const { t } = useTranslation();
const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme();
const { componentCls, hashId } = useStyles();
const parentRoute = useParentRoute();
const { createRoute } = useNocoBaseRoutes();
const insertPageSchema = useInsertFlowPageSchema();
const handleClick = useCallback(async () => {
const values = await FormDialog(
t('Add page'),
() => {
return (
<SchemaComponentOptions scope={options.scope} components={{ ...options.components }}>
<FormLayout layout={'vertical'}>
<SchemaComponent
schema={{
properties: {
title: {
title: t('Menu item title'),
required: true,
'x-component': 'Input',
'x-decorator': 'FormItem',
},
icon: {
title: t('Icon'),
'x-component': 'IconPicker',
'x-decorator': 'FormItem',
},
},
}}
/>
</FormLayout>
</SchemaComponentOptions>
);
},
theme,
).open({
initialValues: {},
});
const menuSchemaUid = uid();
const pageSchemaUid = uid();
const tabSchemaUid = uid();
const tabSchemaName = uid();
// 创建一个路由到 desktopRoutes 表中
await createRoute({
type: NocoBaseDesktopRouteType.page,
title: values.title,
icon: values.icon,
parentId: parentRoute?.id,
schemaUid: pageSchemaUid,
menuSchemaUid,
enableTabs: false,
children: [
{
type: NocoBaseDesktopRouteType.tabs,
schemaUid: tabSchemaUid,
tabSchemaName,
hidden: true,
},
],
});
// 同时插入一个对应的 Schema
insertPageSchema(getFlowPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }));
}, [createRoute, insertPageSchema, options?.components, options?.scope, parentRoute?.id, t, theme]);
return (
<SchemaInitializerItem title={t('Modern page')} onClick={handleClick} className={`${componentCls} ${hashId}`} />
);
};
export function getFlowPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) {
return {
type: 'void',
'x-component': 'FlowPage',
'x-uid': pageSchemaUid,
};
}

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