mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
Merge event-filter
This commit is contained in:
commit
b80d4569b5
@ -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"
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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;
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
@ -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;
|
||||
}
|
@ -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();
|
@ -0,0 +1,9 @@
|
||||
# Model
|
||||
|
||||
## Table Block
|
||||
|
||||
这个示例展示了一个完整的表格组件,包含数据加载、分页、字段配置等功能。
|
||||
|
||||
**使用说明**: 右键点击下方的表格区域,可以打开配置菜单设置显示字段、表格标题等参数。
|
||||
|
||||
<code src="./demos/models/table.tsx"></code>
|
@ -0,0 +1 @@
|
||||
# Flow Actions
|
@ -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();
|
123
packages/core/client/docs/zh-CN/core/flow-engine/flow-action.md
Normal file
123
packages/core/client/docs/zh-CN/core/flow-engine/flow-action.md
Normal 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` 配置参数界面和默认值,提升易用性。
|
242
packages/core/client/docs/zh-CN/core/flow-engine/flow-context.md
Normal file
242
packages/core/client/docs/zh-CN/core/flow-engine/flow-context.md
Normal 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`,可根据实际业务需求自定义属性和方法,但建议遵循上下文只读/可写的设计原则,确保流的可控性和可维护性。
|
@ -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 |
|
||||
|
||||
---
|
@ -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>**
|
||||
获取所有已注册的模型类(构造函数)。返回一个 Map,key 为模型名称,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>
|
@ -0,0 +1,2 @@
|
||||
# FlowHooks
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
```
|
@ -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(); // 从远程删除
|
||||
```
|
@ -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>
|
||||
```
|
200
packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md
Normal file
200
packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md
Normal 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');
|
||||
```
|
@ -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 请求方式和参数。
|
||||
- 仅实现了 GET(refresh),如需扩展可在子类中实现更多操作(如 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();
|
||||
```
|
@ -0,0 +1,6 @@
|
||||
# FlowSettings
|
||||
|
||||
- 悬浮菜单:FlowSettingsDropdown
|
||||
- 右键菜单:FlowSettingsContextMenu
|
||||
- 对话框:FlowSettingsModal
|
||||
- 抽屉:FlowSettingsDrawer
|
@ -0,0 +1 @@
|
||||
# Overview
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -0,0 +1,9 @@
|
||||
import { Application, Plugin } from '@nocobase/client';
|
||||
|
||||
export function createApp({ plugins = [] }: { plugins?: Array<typeof Plugin> } = {}) {
|
||||
const app = new Application({
|
||||
router: { type: 'memory', initialEntries: ['/'] },
|
||||
plugins: [...plugins],
|
||||
});
|
||||
return app.getRootComponent();
|
||||
}
|
@ -0,0 +1,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();
|
@ -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();
|
@ -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();
|
@ -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 () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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());
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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] });
|
@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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();
|
@ -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;
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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();
|
@ -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 () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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,
|
||||
},
|
||||
];
|
||||
});
|
@ -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',
|
||||
},
|
||||
],
|
||||
});
|
@ -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] });
|
@ -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;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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');
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
@ -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();
|
@ -0,0 +1,5 @@
|
||||
# FormFlowModel
|
||||
|
||||
## 演示
|
||||
|
||||
<code src="./demos/form/index.tsx"></code>
|
86
packages/core/client/docs/zh-CN/core/flow-models/index.md
Normal file
86
packages/core/client/docs/zh-CN/core/flow-models/index.md
Normal 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>
|
||||
|
@ -0,0 +1,5 @@
|
||||
# LayoutFlowModel
|
||||
|
||||
## 演示
|
||||
|
||||
<code src="./demos/layout-flow-model.tsx"></code>
|
@ -0,0 +1 @@
|
||||
# LayoutRouteFlowModel
|
@ -0,0 +1 @@
|
||||
# PageFlowModel
|
@ -0,0 +1 @@
|
||||
# PageTabFlowModel
|
269
packages/core/client/docs/zh-CN/core/flow-models/quickstart.md
Normal file
269
packages/core/client/docs/zh-CN/core/flow-models/quickstart.md
Normal 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 中,**一切皆可编排**。
|
@ -0,0 +1,5 @@
|
||||
# TableFlowModel
|
||||
|
||||
## 演示
|
||||
|
||||
<code src="./demos/table/index.tsx"></code>
|
@ -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 = () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
10
packages/core/client/src/block-configs/index.ts
Normal file
10
packages/core/client/src/block-configs/index.ts
Normal 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';
|
14
packages/core/client/src/flow/FlowEngineRunner.tsx
Normal file
14
packages/core/client/src/flow/FlowEngineRunner.tsx
Normal 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}</>;
|
||||
};
|
79
packages/core/client/src/flow/FlowModelRepository.ts
Normal file
79
packages/core/client/src/flow/FlowModelRepository.ts
Normal 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;
|
||||
}
|
||||
}
|
55
packages/core/client/src/flow/FlowPage.tsx
Normal file
55
packages/core/client/src/flow/FlowPage.tsx
Normal 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} />;
|
||||
};
|
55
packages/core/client/src/flow/index.ts
Normal file
55
packages/core/client/src/flow/index.ts
Normal 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';
|
12
packages/core/client/src/flow/models/BlockFlowModel.tsx
Normal file
12
packages/core/client/src/flow/models/BlockFlowModel.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* This file is part of the NocoBase (R) project.
|
||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||
* Authors: NocoBase Team.
|
||||
*
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { DefaultStructure, FlowModel } from '@nocobase/flow-engine';
|
||||
|
||||
export class BlockFlowModel<T = DefaultStructure> extends FlowModel<T> {}
|
80
packages/core/client/src/flow/models/BlockGridFlowModel.tsx
Normal file
80
packages/core/client/src/flow/models/BlockGridFlowModel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
188
packages/core/client/src/flow/models/CalendarBlockFlowModel.tsx
Normal file
188
packages/core/client/src/flow/models/CalendarBlockFlowModel.tsx
Normal 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',
|
||||
},
|
||||
});
|
6
packages/core/client/src/flow/models/FieldFlowModel.tsx
Normal file
6
packages/core/client/src/flow/models/FieldFlowModel.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { Field, FlowModel } from '@nocobase/flow-engine';
|
||||
|
||||
export class FieldFlowModel extends FlowModel {
|
||||
field: Field;
|
||||
fieldPath: string;
|
||||
}
|
75
packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx
Normal file
75
packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx
Normal 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);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
99
packages/core/client/src/flow/models/PageFlowModel.tsx
Normal file
99
packages/core/client/src/flow/models/PageFlowModel.tsx
Normal 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);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
40
packages/core/client/src/flow/models/PageTabFlowModel.tsx
Normal file
40
packages/core/client/src/flow/models/PageTabFlowModel.tsx
Normal 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());
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
58
packages/core/client/src/flow/models/action-model.tsx
Normal file
58
packages/core/client/src/flow/models/action-model.tsx
Normal 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 () => {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
51
packages/core/client/src/flow/models/form-item-model.tsx
Normal file
51
packages/core/client/src/flow/models/form-item-model.tsx
Normal 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;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
141
packages/core/client/src/flow/models/form-model.tsx
Normal file
141
packages/core/client/src/flow/models/form-model.tsx
Normal 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',
|
||||
},
|
||||
});
|
22
packages/core/client/src/flow/models/index.ts
Normal file
22
packages/core/client/src/flow/models/index.ts
Normal 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';
|
||||
//
|
37
packages/core/client/src/flow/models/submit-action-model.tsx
Normal file
37
packages/core/client/src/flow/models/submit-action-model.tsx
Normal 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();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
125
packages/core/client/src/flow/models/table-column-model.tsx
Normal file
125
packages/core/client/src/flow/models/table-column-model.tsx
Normal 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;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
112
packages/core/client/src/flow/models/table-model.tsx
Normal file
112
packages/core/client/src/flow/models/table-model.tsx
Normal 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',
|
||||
},
|
||||
});
|
@ -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';
|
||||
|
121
packages/core/client/src/modules/menu/FlowPageMenuItem.tsx
Normal file
121
packages/core/client/src/modules/menu/FlowPageMenuItem.tsx
Normal 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
Loading…
x
Reference in New Issue
Block a user