diff --git a/package.json b/package.json index ed756df09e..5ebd680ca5 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/packages/core/client/.dumirc.ts b/packages/core/client/.dumirc.ts index 8389585d18..1505125391 100644 --- a/packages/core/client/.dumirc.ts +++ b/packages/core/client/.dumirc.ts @@ -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', diff --git a/packages/core/client/docs/zh-CN/core/event-and-filter/demos/EventFilterTableDemo2.tsx b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/EventFilterTableDemo2.tsx new file mode 100644 index 0000000000..18e3621295 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/EventFilterTableDemo2.tsx @@ -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, +// eventSteps: Record +// }[] = [ +// { +// 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 = { +// 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 = { +// 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 = { +// 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 = { +// 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 = { +// 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 = { +// 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 = { +// 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 = { +// 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 ( +// +// +// +// +// {/* */} +// +// ); +// }; + +// // 图标映射 +// const IconComponents = { +// RedoOutlined, +// EyeOutlined, +// PlusOutlined, +// DeleteOutlined +// }; + +// interface ToolbarActionProps { +// event: string; +// filterSteps: Record; +// eventSteps: Record; +// } + +// 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 ( +// +// ); +// }; + +// // 主表格组件 +// 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 : ( +// <> +// +// +// {actions.toolbar.map((action: any) => ( +// +// ))} +// +// +// +// +// ) +// } +// +// ); +// }; + +// // 主组件 +// const EventFilterTableDemo2: React.FC = () => { +// return ( +// +// +// +// +// +// +// +// +// +// +// ); +// }; + +// export default EventFilterTableDemo2; diff --git a/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/collections.json b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/collections.json new file mode 100644 index 0000000000..4d3aa45f83 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/collections.json @@ -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 + } + ] + } +] diff --git a/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/createApp.tsx b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/createApp.tsx new file mode 100644 index 0000000000..127f2e7c8d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/data-source/demos/createApp.tsx @@ -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, + options: ApplicationOptions = {}, + mocks: Record = 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; +} diff --git a/packages/core/client/docs/zh-CN/core/event-and-filter/demos/models/table.tsx b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/models/table.tsx new file mode 100644 index 0000000000..a40e3d37cf --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/event-and-filter/demos/models/table.tsx @@ -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 ( +
+ + + + + +
+ ); +}; + +// 表格组件 +const TableComponent = ({ + loading = false, + columns = [], + dataSource = [], + pagination = { current: 1, pageSize: 10, total: 0 }, + height = 400, + title = '数据表格', + onPaginationChange, +}) => { + return ( +
+
+

{title}

+
+ + +
+ + + {pagination.total > 0 && ( +
+ `显示 ${range[0]}-${range[1]} 条,共 ${total} 条数据`} + onChange={onPaginationChange} + onShowSizeChange={onPaginationChange} + /> +
+ )} + + ); +}; + +const ActionButton = withFlowModel( + (props: ButtonProps & { text?: string }) => { + const { text, ...rest } = props; + return ; + }, + { + settings: { + component: FlowsFloatContextMenu, + // component: FlowsContextMenu + }, + }, +); + +// Actions组件 - 渲染工具栏操作 +const ActionsComponent = observer(({ model }: { model: BlockModel }) => { + if (!model.actions.size) return null; + + return ( +
+ + {Array.from(model.actions.values()).map((action) => ( + + ))} + +
+ ); +}); + +const TableBlock = withFlowModel(TableComponent); + +// 创建继承自BlockModel的DemoTableBlockModel +class DemoTableBlockModel extends BlockModel { + private resources: Map = 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(); diff --git a/packages/core/client/docs/zh-CN/core/event-and-filter/model.md b/packages/core/client/docs/zh-CN/core/event-and-filter/model.md new file mode 100644 index 0000000000..49094e97ee --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/event-and-filter/model.md @@ -0,0 +1,9 @@ +# Model + +## Table Block + +这个示例展示了一个完整的表格组件,包含数据加载、分页、字段配置等功能。 + +**使用说明**: 右键点击下方的表格区域,可以打开配置菜单设置显示字段、表格标题等参数。 + + \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/flow-actions/index.md b/packages/core/client/docs/zh-CN/core/flow-actions/index.md new file mode 100644 index 0000000000..9555665ae2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-actions/index.md @@ -0,0 +1 @@ +# Flow Actions diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/demos/quickstart.tsx b/packages/core/client/docs/zh-CN/core/flow-engine/demos/quickstart.tsx new file mode 100644 index 0000000000..c917c415a6 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/demos/quickstart.tsx @@ -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 { + // 从本地存储加载模型数据 + 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
{this.props.name}
; + } +} + +// 为 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: , + }); + } +} + +// 创建应用实例,注册插件 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-action.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-action.md new file mode 100644 index 0000000000..712a5e3ce4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-action.md @@ -0,0 +1,123 @@ +# FlowAction + +`FlowAction` 是 NocoBase 流引擎中用于定义和注册流步骤可复用操作(Action)的核心对象。每个操作(Action)封装一段可执行的业务逻辑,可以在多个流步骤中复用,支持参数配置、UI 配置和类型推断。 + +--- + +## ActionDefinition 接口 + +```ts +interface ActionDefinition { + name: string; // 操作唯一标识,必须唯一 + title?: string; // 操作显示名称(可选) + uiSchema?: Record; // (可选)用于参数配置界面渲染 + defaultParams?: Record; // (可选)默认参数 + paramsRequired?: boolean; // (可选)是否需要参数配置,为true时添加模型前会打开配置对话框 + hideInSettings?: boolean; // (可选)是否在设置菜单中隐藏该步骤 + handler: (ctx: FlowContext, params: any) => Promise | 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` 配置参数界面和默认值,提升易用性。 diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-context.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-context.md new file mode 100644 index 0000000000..0a76402e19 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-context.md @@ -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`,可根据实际业务需求自定义属性和方法,但建议遵循上下文只读/可写的设计原则,确保流的可控性和可维护性。 diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md new file mode 100644 index 0000000000..6729c8fa20 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-definition.md @@ -0,0 +1,185 @@ +# FlowDefinition + +`FlowDefinition` 是 NocoBase 流引擎中用于描述和注册流(Flow)的核心定义对象。它用于定义流的唯一标识(`key`)、各个步骤(`steps`)、事件触发条件、参数配置及处理逻辑,是实现流自动化和业务编排的基础。 + +--- + +## 核心结构 + +```ts +interface FlowDefinition { + key: string; // 流唯一标识 + on?: { event: string }; // 可选:事件触发配置 + auto?: boolean; // 可选:是否自动运行 + steps: Record; // 流步骤定义 +} + +interface StepDefinition { + use?: string; // 可选:引用已注册的全局 Action + defaultParams?: any; // 默认参数 + uiSchema?: any; // 可选:用于 FlowSettings 配置界面 + handler?: (ctx: any, params: any) => Promise; // 可选:步骤处理函数 +} +``` + +--- + +## 定义流方式 + +### 函数式定义(适合简单流) + +结构清晰、类型推断友好,推荐用于大多数场景。 + +```ts +type MyFlowSteps = { + step1: { name: string }; + step2: { age: number }; +}; + +const myFlow = defineFlow({ + 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({ + 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` | 步骤集合,键为步骤名,值为步骤定义 | + +### StepDefinition 配置速查表 + +| 字段 | 类型 | 说明 | +| --------------- | -------------------------------------- | ------------------------------------- | +| `use` | `string` | (可选)引用已注册的全局 Action | +| `defaultParams` | `any` | 步骤的默认参数 | +| `uiSchema` | `any` | (可选)用于 FlowSettings UI 渲染 | +| `handler` | `(ctx, params) => Promise` | (可选)步骤执行逻辑,若未定义则使用 `use` 指定的全局 Action | + +--- diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-engine.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-engine.md new file mode 100644 index 0000000000..711796319f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-engine.md @@ -0,0 +1,89 @@ +# FlowEngine + +`FlowEngine` 是 NocoBase 前端流引擎的核心调度与管理类,专为前端流自动化与业务逻辑编排设计。它负责流模型(Model)与操作(Action)的注册、生命周期管理、持久化、远程同步等,为前端场景下的流运行和扩展提供统一的环境与机制。 + +--- + +## 主要方法与属性 + +### Model 类注册与管理 + +- **registerModels(models: Record): void** + 批量注册模型类。 + +- **getModelClass(name: string): ModelConstructor | undefined** + 获取已注册的模型类。 + +- **getModelClasses(): Map** + 获取所有已注册的模型类(构造函数)。返回一个 Map,key 为模型名称,value 为对应的模型类。常用于遍历、动态生成模型列表或批量操作等场景。 + +--- + +### Model 实例管理 + +- **createModel\(options: CreateModelOptions): T** + 创建并注册一个模型实例。如果指定 UID 已存在,则返回现有实例。支持泛型以确保正确的模型类型推导。 + +- **getModel\(uid: string): T | undefined** + 根据 UID 获取本地模型实例。 + +- **removeModel(uid: string): boolean** + 销毁并移除一个本地模型实例。 + +--- + +### Model 持久化与远程操作 + +- **setModelRepository(modelRepository: IFlowModelRepository): void** + 注入模型仓库(通常用于远程数据源/持久化适配器)。 + +- **async loadModel\(uid: string): Promise\** + 从远程仓库加载模型数据,并创建本地实例。如果模型不存在则返回 null。 + +- **async loadOrCreateModel\(options: CreateModelOptions): Promise\** + 从远程仓库加载模型,如果不存在则创建新模型实例并持久化。如果仓库未设置则返回 null。 + +- **async saveModel(model: FlowModel): Promise** + 保存模型到远程仓库。 + +- **async destroyModel(uid: string): Promise** + 从远程仓库删除模型,并移除本地实例。 + +--- + +### Action 注册与获取 + +- **registerAction\(nameOrDefinition, options?): void** + 注册一个 Action,可传入名称和选项,或完整的 ActionDefinition 对象。支持泛型以确保正确的模型类型推导。Action 是流中的可复用操作单元。 + +- **getAction\(name: string): ActionDefinition\ | undefined** + 获取已注册的 Action 定义。支持泛型以确保正确的模型类型推导。 + +--- + +### Flow 注册 + +- **registerFlow\(modelClassName: string, flowDefinition: FlowDefinition\): void** + 注册一个 Flow 到指定的模型类。 + +--- + +### FlowSettings 管理 + +- **flowSettings.registerComponents(components): void** + 添加组件到 flowSettings 的组件注册表中, 这些组件可以在 flow step 的 uiSchema 中使用。 + +- **flowSettings.registerScopes(scopes): void** + 添加作用域到 flowSettings 的作用域注册表中, 这些作用域可以在 flow step 的 uiSchema 中使用。 + +- **flowSettings.load(): Promise\** + 加载 FlowSettings 相关资源,未启用 FlowSettings 时可不调用。 + +- **flowSettings.openStepSettingsDialog(props: StepSettingsDialogProps)** + 显示单个步骤的配置界面。 + +--- + +## 示例 + + diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-hooks.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-hooks.md new file mode 100644 index 0000000000..00ab2e1a45 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-hooks.md @@ -0,0 +1,2 @@ +# FlowHooks + diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md new file mode 100644 index 0000000000..76a4281330 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-renderer.md @@ -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 + + /** 是否为每个组件独立执行 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 +// 基础示例 + + +// 显示 flow settings + + +// 显示设置但隐藏移除按钮 + + +// 跳过自动应用流 + + +// 传递额外上下文 + + +// 完整配置示例 + +``` + +## 使用场景 + +### 1. 基础渲染 +当只需要渲染流模型内容时,使用最简配置: + +```tsx | pure + +``` + +### 2. 带设置功能的渲染 +当需要用户能够配置流时,启用流设置: + +```tsx | pure + +``` + +### 3. 带设置但禁用删除功能 +当需要流设置但不允许用户删除时: + +```tsx | pure + +``` + +### 4. 自定义流控制 +当需要手动控制流应用时,可以跳过自动流: + +```tsx | pure + +``` + +### 5. 传递自定义上下文 +当需要向流传递特定上下文数据时: + +```tsx | pure + +``` diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-repository.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-repository.md new file mode 100644 index 0000000000..c46c7fc75d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-repository.md @@ -0,0 +1,72 @@ +# IFlowModelRepository + +`IFlowModelRepository` 是 FlowEngine 的模型持久化接口,定义了模型的远程加载、保存和删除等操作。通过实现该接口,可以将模型的数据持久化到后端数据库、API 或其他存储介质,实现前后端的数据同步。 + +## 主要方法 + +- **load(uid: string): Promise** + 根据唯一标识符 uid 从远程加载模型数据。 + +- **save(model: FlowModel): Promise** + 将模型数据保存到远程存储。 + +- **destroy(uid: string): Promise** + 根据 uid 从远程存储删除模型。 + +## FlowModelRepository 示例 + +```ts +class FlowModelRepository implements IFlowModelRepository { + 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(); // 从远程删除 +``` diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-settings.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-settings.md new file mode 100644 index 0000000000..f6c51da213 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model-settings.md @@ -0,0 +1,41 @@ +# FlowModelSettings + +`FlowModelSettings` 是用于管理和配置 FlowModel 上所有流(Flow)的专用组件。它为用户提供了多种交互入口,方便在不同场景下快捷地查看、编辑和管理模型的流设置。 + +--- + +## 常见用法 + +除了和 FlowModelRenderer 集成,也可以单独使用,例如: + +### 悬浮菜单 + +```tsx | pure + + Click me + +``` + +### 右键菜单 + +```tsx | pure + + Click me + +``` + +### 对话框 + +```tsx | pure + + Click me + +``` + +### 抽屉 + +```tsx | pure + + Click me + +``` diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md new file mode 100644 index 0000000000..e65d659b5b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-model.md @@ -0,0 +1,200 @@ +# FlowModel + +`FlowModel` 是 NocoBase 流引擎的基础模型类,支持流注册、流执行、属性管理、子模型管理、持久化等功能。所有业务模型均可继承自 FlowModel。 + +## 泛型支持 + +FlowModel 支持泛型,可以通过类型参数定义模型的结构,提供更好的类型安全和智能提示。 + +### 基本泛型用法 + +```ts +interface MyModelStructure { + parent?: ParentModel; + subModels?: { + tabs?: TabModel[]; + items?: ItemModel[]; + }; +} + +class MyModel extends FlowModel { + // 现在 this.parent 和 this.subModels 都有正确的类型推导 +} +``` + +### 默认结构类型 + +如果不指定泛型参数,FlowModel 使用默认的 `DefaultStructure`: + +```ts +interface DefaultStructure { + parent?: FlowModel | null; + subModels?: Record; +} +``` + +--- + +## 主要属性 + +- **uid: string** + 模型唯一标识符。 + +- **props: IModelComponentProps** + 组件属性,支持响应式。 + +- **stepParams: StepParams** + 流步骤参数。 + +- **flowEngine: FlowEngine** + 关联的流引擎实例。 + +- **parent: Structure['parent']** + 父模型实例,类型由泛型 Structure 决定。默认为 `FlowModel | null`。 + +- **subModels: Structure['subModels']** + 子模型集合,类型由泛型 Structure 决定。默认为 `Record`。 + 支持对象字段(如 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\(keyOrDefinition, flowDefinition?)** + 配置流,支持字符串 key 或完整对象。支持泛型以确保类型安全。 + +- **static extendFlow\(keyOrDefinition, extendDefinition?)** + 扩展已存在的流程定义,通过合并现有流程和扩展定义来创建新的流程。 + +- **applyFlow(flowKey: string, extra?: FlowExtraContext): Promise** + 执行指定流。 + +- **dispatchEvent(eventName: string, extra?: FlowExtraContext): void** + 触发事件,自动匹配并执行相关流。 + +- **applyAutoFlows(extra?: FlowExtraContext): Promise** + 执行所有自动应用流。 + +- **getFlow(key: string): FlowDefinition \| undefined** + 获取指定 key 的流配置。 + +- **static getFlows(): Map** + 获取所有已配置流(含继承)。 + +- **getAutoFlows(): FlowDefinition[]** + 获取所有自动应用流程定义并按 sort 排序。 + +--- + +### 步骤设置 + +- **openStepSettingsDialog(flowKey: string, stepKey: string)** + 打开步骤设置对话框。 + +- **async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string): Promise** + 配置必填步骤参数。用于在一个分步表单中配置所有需要参数的步骤。 + - `dialogWidth`: 对话框宽度,默认为 800 + - `dialogTitle`: 对话框标题,默认为 '步骤参数配置' + - 返回表单提交的值 + +--- + +### 子模型管理 + +- **setSubModel(subKey: string, options): FlowModel** + 创建并设置一个子模型到对象字段(如 detail、config)。 + +- **addSubModel(subKey: string, options): FlowModel** + 创建并添加一个子模型到数组字段(如 tabs、columns)。 + +- **mapSubModels(subKey: K, callback: (model) => R): R[]** + 遍历指定 key 的子模型,对每个子模型执行 callback 函数,并返回结果数组。 + - 支持完整的类型推导,callback 参数会自动推导为正确的模型类型 + - 如果子模型不存在,返回 null + - 自动处理单个模型和模型数组的情况 + +- **setParent(parent: FlowModel): void** + 设置父模型。 + +- **createRootModel(options): FlowModel** + 通过 flowEngine 创建根模型。 + +--- + +### 持久化与销毁 + +- **async save(): Promise** + 保存模型到远程。 + +- **async destroy(): Promise** + 删除模型。 + +--- + +### 渲染 + +- **render(): React.ReactNode | Function** + 渲染模型的 React 组件,默认返回空 div,建议子类重写。 + +--- + +## 主要示例 + +```ts +class MyModel extends FlowModel { + onInit(options) { + // 初始化逻辑 + } + render() { + return
{this.props.name}
; + } +} + +// 为 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'); +``` diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-resource.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-resource.md new file mode 100644 index 0000000000..60ba6fb19a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-resource.md @@ -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(); +``` diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-settings/demos/FlowsContextMenu.tsx b/packages/core/client/docs/zh-CN/core/flow-engine/flow-settings/demos/FlowsContextMenu.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/flow-settings/index.md b/packages/core/client/docs/zh-CN/core/flow-engine/flow-settings/index.md new file mode 100644 index 0000000000..52970b66bc --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/flow-settings/index.md @@ -0,0 +1,6 @@ +# FlowSettings + +- 悬浮菜单:FlowSettingsDropdown +- 右键菜单:FlowSettingsContextMenu +- 对话框:FlowSettingsModal +- 抽屉:FlowSettingsDrawer diff --git a/packages/core/client/docs/zh-CN/core/flow-engine/index.md b/packages/core/client/docs/zh-CN/core/flow-engine/index.md new file mode 100644 index 0000000000..07dd0c5c77 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-engine/index.md @@ -0,0 +1 @@ +# Overview diff --git a/packages/core/client/docs/zh-CN/core/flow-models/block-grid-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/block-grid-flow-model.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsContextMenu.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsContextMenu.tsx new file mode 100644 index 0000000000..2de5599c12 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsContextMenu.tsx @@ -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 Hello {name}; + } +} + +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: ( +
+ + + +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsFloatContextMenu.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsFloatContextMenu.tsx new file mode 100644 index 0000000000..9e5d32bc7a --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/FlowsFloatContextMenu.tsx @@ -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 Hello {name}; + } +} + +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: ( +
+ + + +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/action.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/action.tsx new file mode 100644 index 0000000000..61860fdd89 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/action.tsx @@ -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 ( +
+ +
+ ); +}; + +const ButtonComponent = (props: ButtonProps & { text?: string }) => { + const { text, ...rest } = props; + return ; +}; + +// 使用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(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/array-resource.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/array-resource.tsx new file mode 100644 index 0000000000..1fba286ad8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/array-resource.tsx @@ -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 ( +
+
{JSON.stringify(this.resource.getData(), null, 2)}
+ + + + +
+ ); + } +} + +// 插件定义 +class PluginTableBlockModel extends Plugin { + async load() { + this.flowEngine.registerModels({ ArrayResourceFlowModel }); + const model = this.flowEngine.createModel({ + use: 'ArrayResourceFlowModel', + }); + this.router.add('root', { + path: '/', + element: ( +
+ +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/collection-inherits.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/collection-inherits.tsx new file mode 100644 index 0000000000..e3d1506dbe --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/collection-inherits.tsx @@ -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 ( +
+ + {this.collection.getFields().map((field) => ( +
{field.name}
+ ))} + + + + +
+
+ ); + } +} + +class PluginTableBlockModel extends Plugin { + async load() { + this.flowEngine.registerModels({ ConfigureFieldsFlowModel }); + const model = this.flowEngine.createModel({ + use: 'ConfigureFieldsFlowModel', + }); + this.router.add('root', { + path: '/', + element: , + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/configure-fields.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/configure-fields.tsx new file mode 100644 index 0000000000..a08a030e27 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/configure-fields.tsx @@ -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 ( +
+ { + 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; + }} + /> +
+ ); + } +} + +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 { + collection: Collection; + + getFieldMenuItems() { + return this.collection.mapFields((field) => { + return { + key: `${this.collection.dataSource.name}.${this.collection.name}.${field.name}`, + label: field.title, + }; + }); + } + + render() { + return ( +
+ {this.mapSubModels('fields', (field) => ( + + ))} + { + const model = this.addSubModel('fields', { + use: 'FieldModel', + stepParams: { + default: { + step1: { + fieldPath: info.key, + }, + }, + }, + }); + }, + }} + > + + +
+ ); + } +} + +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: , + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/createApp.ts b/packages/core/client/docs/zh-CN/core/flow-models/demos/createApp.ts new file mode 100644 index 0000000000..e55b8220f4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/createApp.ts @@ -0,0 +1,9 @@ +import { Application, Plugin } from '@nocobase/client'; + +export function createApp({ plugins = [] }: { plugins?: Array } = {}) { + const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [...plugins], + }); + return app.getRootComponent(); +} diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/data-source.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/data-source.tsx new file mode 100644 index 0000000000..430791a8a4 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/data-source.tsx @@ -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 ( +
+ {this.getDataSources().map((ds) => ( + + {ds.getCollections().map((collection) => ( + + {collection.getFields().map((field) => ( +
{field.name}
+ ))} + + + + +
+ ))} + + + + +
+ ))} + + + + +
+ ); + } +} + +class PluginTableBlockModel extends Plugin { + async load() { + this.flowEngine.registerModels({ ConfigureFieldsFlowModel }); + const model = this.flowEngine.createModel({ + use: 'ConfigureFieldsFlowModel', + }); + this.router.add('root', { + path: '/', + element: , + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/dispatch-event.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/dispatch-event.tsx new file mode 100644 index 0000000000..65fcbfd131 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/dispatch-event.tsx @@ -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 ( +
{ + this.dispatchEvent('event1', { event }); + }} + > + Hello {name} +
+ ); + } +} + +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/echarts.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/echarts.tsx new file mode 100644 index 0000000000..89c6974b15 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/echarts.tsx @@ -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(ref: React.RefObject, 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(); + render() { + return ( + +
+ + ); + } +} + +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: ( +
+ +
+ +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/form/action-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/action-model.tsx new file mode 100644 index 0000000000..a3f6318047 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/action-model.tsx @@ -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 {this.props.title || 'Action'}; + } +} + +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 () => {}, + }); + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-item-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-item-model.tsx new file mode 100644 index 0000000000..6f45c169d9 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-item-model.tsx @@ -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 ( +
+ +
+ ); + } +} + +FormItemModel.registerFlow({ + key: 'default', + auto: true, + steps: { + step1: { + handler(ctx, params) { + const field = ctx.globals.dsm.getCollectionField(params.fieldPath); + ctx.model.field = field; + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-model.tsx new file mode 100644 index 0000000000..28d9e066df --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/form-model.tsx @@ -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 ( +
+ + {this.mapSubModels('fields', (field) => ( + + ))} + + {this.mapSubModels('actions', (action) => ( + + ))} + +
+ +
{JSON.stringify(this.form.values, null, 2)}
+
+
+
+ ); + } + + async openDialog({ filterByTk }) { + return new Promise((resolve) => { + const dialog = FormDialog( + { + footer: null, + title: 'Form Dialog', + }, + (form) => { + return ( +
+ + + + { + await this.resource.save(this.form.values); + dialog.close(); + resolve(this.form.values); // 在 close 之后 resolve + }} + > + Submit + + + +
+ ); + }, + ); + 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()); + } + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/form/index.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/index.tsx new file mode 100644 index 0000000000..f05fd11865 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/index.tsx @@ -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: , + }); + } +} + +export default createApp({ plugins: [PluginDemo] }); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/form/submit-action-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/submit-action-model.tsx new file mode 100644 index 0000000000..c7b48002dc --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/form/submit-action-model.tsx @@ -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 {this.props.title || '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(); + } + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/formily.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/formily.tsx new file mode 100644 index 0000000000..5f7266f654 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/formily.tsx @@ -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 ( + + + + Submit + +
+ +
{JSON.stringify(this.form.values, null, 2)}
+
+
+ ); + } +} + +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: ( +
+ +
+ +
+ ), + }); + } +} + +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginFormilyModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/grid.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/grid.tsx new file mode 100644 index 0000000000..1d69e6aa71 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/grid.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +interface GridProps { + items: string[][][]; // 三维数组:行-列-区块 + itemRender: (uid: string) => React.ReactNode; +} + +function Grid({ items, itemRender }: GridProps) { + return ( +
+ {items.map((row, rowIdx) => ( +
+ {row.map((col, colIdx) => ( +
+ {col.map((uid, blockIdx) => ( +
+ {itemRender(uid)} +
+ ))} +
+ ))} +
+ ))} +
+ ); +} + +const items = [ + [ + // 第一行 + ['A', 'B'], // 第一行的第一列两个区块 + ['C'], // 第一行的第二列一个区块 + ], + [ + // 第二行 + ['D'], // 第二行的第一列一个区块 + ['E', 'F'], // 第二行的第二列两个区块 + ], +]; + +function GridDemo() { + return ( +
+
{uid}
} /> +
+ ); +} + +export default GridDemo; diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/hello-set-props.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/hello-set-props.tsx new file mode 100644 index 0000000000..464ac2464e --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/hello-set-props.tsx @@ -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 ( +
+
+ Hello {name} +
+ { + this.props.name = e.target.value; + }} + /> +
+ ); + } +} + +// 插件定义 +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/hello.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/hello.tsx new file mode 100644 index 0000000000..cefcf68a77 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/hello.tsx @@ -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
Hello {name}
; + } +} + +// 插件定义 +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/layout-flow-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/layout-flow-model.tsx new file mode 100644 index 0000000000..29c4381dc3 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/layout-flow-model.tsx @@ -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 ( +
+ +
+ ); + } +} + +// 插件定义 +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/markdown.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/markdown.tsx new file mode 100644 index 0000000000..f32190ba2f --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/markdown.tsx @@ -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(uid, 'MarkdownModel'); + return ( +
+ +
+ ); +}; + +const Markdown = ({ content, height }) => { + if (content === undefined || content === null) { + return
Loading content or no content set...
; + } + return
; +}; + +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(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/model-repository.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/model-repository.tsx new file mode 100644 index 0000000000..7282b847f2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/model-repository.tsx @@ -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 { + 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(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource-table.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource-table.tsx new file mode 100644 index 0000000000..644ee06c3e --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource-table.tsx @@ -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 ( +
+ { + 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; + }} + /> +
+ ); + } +} + +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 { + collection: Collection; + + render() { + return ( +
+
+ + ); + } +} + +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: , + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource.tsx new file mode 100644 index 0000000000..1b422bb356 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/multi-record-resource.tsx @@ -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 ( +
+
+ Resource: {this.resource.getResourceName()} | + Page: {this.resource.getPage()} | + PageSize: {this.resource.getPageSize()} | + Total: {responseMeta.total || 0} +
+
{JSON.stringify(data, null, 2)}
+ + + + + + + + + + +
+ ); + } +} + +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: ( +
+ +
+ ), + }); + } +} + +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginMultiRecordDemo], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/object-resource.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/object-resource.tsx new file mode 100644 index 0000000000..fdfc1fbb69 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/object-resource.tsx @@ -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 ( +
+
{JSON.stringify(this.resource.getData(), null, 2)}
+ +
+ ); + } +} + +// 插件定义 +class PluginTableBlockModel extends Plugin { + async load() { + this.flowEngine.registerModels({ ObjectResourceFlowModel }); + const model = this.flowEngine.createModel({ + use: 'ObjectResourceFlowModel', + }); + this.router.add('root', { + path: '/', + element: ( +
+ +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/on-init.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/on-init.tsx new file mode 100644 index 0000000000..532c60512c --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/on-init.tsx @@ -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; + + onInit(options: any) { + this.tabs = observable(options.tabs || []); + } + + addTab(tab: any) { + this.tabs.push(tab); + } + + render() { + return ( +
+ this.addTab({ key: uid(), label: `Tab -${uid()}` })}>Add Tab + } + /> +
+ ); + } +} + +// 插件定义 +class PluginHelloModel extends Plugin { + async load() { + this.flowEngine.registerModels({ HelloFlowModel }); + const model = this.flowEngine.createModel({ + use: 'HelloFlowModel', + tabs: [ + { key: uid(), label: 'Tab 1' }, + { key: uid(), label: 'Tab 2' }, + ], + }); + this.router.add('root', { path: '/', element: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/open-required-step-params-dialog.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/open-required-step-params-dialog.tsx new file mode 100644 index 0000000000..5ba57ceed8 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/open-required-step-params-dialog.tsx @@ -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 ( +
+ +

+ 这个演示展示了 configureRequiredSteps 方法的使用。 + 该方法会在一个分步表单对话框中显示所有标记为 paramsRequired: true 的步骤。 +

+ + + + +
+

流程说明:

+
    +
  • setDataSource: 数据源配置 (必填)
  • +
  • setCollection: 数据表配置 (必填)
  • +
  • setTitle: 标题配置 (必填)
  • +
  • setOptionalConfig: 可选配置 (跳过)
  • +
+

+ 点击"创建模型并配置必填参数"按钮后,系统会在一个分步表单对话框中 + 显示前三个必填步骤,配置完成后会在卡片中显示当前的步骤参数。 +

+ {model && JSON.stringify(model.stepParams || {})} +
+
+
+
+ ); +}; + +// 插件定义 +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(); \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/quickstart-1-basic.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/quickstart-1-basic.tsx new file mode 100644 index 0000000000..bbb23e12a1 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/quickstart-1-basic.tsx @@ -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 + + + + + + ); + } +} + +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: ( +
+ +
+ ), + }); + } +} + +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginSingleRecordDemo], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/sub-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/sub-model.tsx new file mode 100644 index 0000000000..411a1ee211 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/sub-model.tsx @@ -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> { + 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 ( +
+ ({ + key: tab.getProps().key, + label: tab.getProps().label, + children: tab.render() + }))} + tabBarExtraContent={ + + } + /> +
+ ); + } +} + +// 插件定义 +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table-block.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table-block.tsx new file mode 100644 index 0000000000..f3ef6cd0f2 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table-block.tsx @@ -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 + + ), + } as any); + } + + addAction(action) { + return this.addSubModel('actions', action); + } + + renderActions() { + return ( + + {this.subModels.actions.map((action) => { + return ; + })} + { + this.addAction({ + use: 'ActionFlowModel', + props: { + // type: 'primary', + children: `新动作 ${uid()}`, + // onClick: async () => {}, + }, + }); + }, + items: [ + { key: 'add-new', label: 'Add new' }, + { key: 'edit', label: 'Edit' }, + ], + }} + > + + + + ); + } + + render() { + return ( +
+ {this.renderActions()} +
+
+
+ + ); + } +} + +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table-flow-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table-flow-model.tsx new file mode 100644 index 0000000000..086a71883d --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table-flow-model.tsx @@ -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 + + ), + } as any); + } + + addAction(action) { + return this.addSubModel('actions', action); + } + + renderActions() { + return ( + + {this.subModels.actions.map((action) => { + return ; + })} + { + this.addAction({ + use: 'ActionFlowModel', + props: { + // type: 'primary', + children: `新动作 ${uid()}`, + // onClick: async () => {}, + }, + }); + }, + items: [ + { key: 'add-new', label: 'Add new' }, + { key: 'edit', label: 'Edit' }, + ], + }} + > + + + + ); + } + + render() { + return ( +
+ {this.renderActions()} +
+
+
+ + ); + } +} + +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: }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginTableBlockModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/action-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/action-model.tsx new file mode 100644 index 0000000000..b9fca7ef19 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/action-model.tsx @@ -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 {this.props.title || 'Untitle'}; + } +} + +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 () => {}, + }); + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/api.ts b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/api.ts new file mode 100644 index 0000000000..7b0f733807 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/api.ts @@ -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, + }, + ]; +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/data-source-manager.ts b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/data-source-manager.ts new file mode 100644 index 0000000000..3bb61f4475 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/data-source-manager.ts @@ -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', + }, + ], +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/index.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/index.tsx new file mode 100644 index 0000000000..b1734d0346 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/index.tsx @@ -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: , + }); + } +} + +export default createApp({ plugins: [PluginDemo] }); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-column-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-column-model.tsx new file mode 100644 index 0000000000..4e405e94ec --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-column-model.tsx @@ -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) => ( + + {value} + { + 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); + }} + /> + + ); + } +} + +export class TableColumnActionsModel extends TableColumnModel { + getColumnProps() { + return { title: 'Actions', ...this.props, render: this.render() }; + } + + render() { + return (value, record, index) => ( + + {this.mapSubModels('actions', (action: ActionModel) => ( + + ))} + + ); + } +} + +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; + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx new file mode 100644 index 0000000000..a160cadf56 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/table/table-model.tsx @@ -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 { + collection: Collection; + resource: MultiRecordResource; + + getColumns() { + return this.mapSubModels('columns', (column) => column.getColumnProps()).concat({ + key: 'addColumn', + fixed: 'right', + title: ( + { + 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, + }; + }), + }} + > + + + ), + } as any); + } + + render() { + return ( +
+
{ + this.resource.setPage(pagination.current); + this.resource.setPageSize(pagination.pageSize); + this.resource.refresh(); + }} + /> + + ); + } +} + +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'); + }, + }, + }, +}); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/demos/tabulator.tsx b/packages/core/client/docs/zh-CN/core/flow-models/demos/tabulator.tsx new file mode 100644 index 0000000000..ebea4d9eda --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/demos/tabulator.tsx @@ -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(ref: React.RefObject, 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(); + render() { + return ( + +
+ + ); + } +} + +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: ( +
+ +
+ +
+ ), + }); + } +} + +// 创建应用实例 +const app = new Application({ + router: { type: 'memory', initialEntries: ['/'] }, + plugins: [PluginHelloModel], +}); + +export default app.getRootComponent(); diff --git a/packages/core/client/docs/zh-CN/core/flow-models/form-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/form-flow-model.md new file mode 100644 index 0000000000..707c8beeec --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/form-flow-model.md @@ -0,0 +1,5 @@ +# FormFlowModel + +## 演示 + + diff --git a/packages/core/client/docs/zh-CN/core/flow-models/index.md b/packages/core/client/docs/zh-CN/core/flow-models/index.md new file mode 100644 index 0000000000..7a4ed32b46 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/index.md @@ -0,0 +1,86 @@ +# Flow Models + +## Basic + + + +## Set props + + + +## onInit + + + +## sub-model + + + +## register flow + + + +## table block + + + +## react-big-calendar + + + +## formily + + + +## ref + + + +## grid + + + +## FlowsFloatContextMenu + + + +## FlowsContextMenu + + + +## 事件触发 + + + +## 数据源 + + + +## collection inherits + + + +## configure fields + + + +## Array Resource + + + +## Object Resource + + + +## Single Record Resource + + + +## Multi Record Resource + + + +## 必填参数配置对话框 + + + diff --git a/packages/core/client/docs/zh-CN/core/flow-models/layout-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/layout-flow-model.md new file mode 100644 index 0000000000..ef45f072db --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/layout-flow-model.md @@ -0,0 +1,5 @@ +# LayoutFlowModel + +## 演示 + + diff --git a/packages/core/client/docs/zh-CN/core/flow-models/layout-route-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/layout-route-flow-model.md new file mode 100644 index 0000000000..1f44ee52aa --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/layout-route-flow-model.md @@ -0,0 +1 @@ +# LayoutRouteFlowModel \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/flow-models/page-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/page-flow-model.md new file mode 100644 index 0000000000..eaac39e72b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/page-flow-model.md @@ -0,0 +1 @@ +# PageFlowModel \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/flow-models/page-tab-flow-model.md b/packages/core/client/docs/zh-CN/core/flow-models/page-tab-flow-model.md new file mode 100644 index 0000000000..93f2f69d30 --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/page-tab-flow-model.md @@ -0,0 +1 @@ +# PageTabFlowModel \ No newline at end of file diff --git a/packages/core/client/docs/zh-CN/core/flow-models/quickstart.md b/packages/core/client/docs/zh-CN/core/flow-models/quickstart.md new file mode 100644 index 0000000000..65c91f581b --- /dev/null +++ b/packages/core/client/docs/zh-CN/core/flow-models/quickstart.md @@ -0,0 +1,269 @@ +# 快速开始:用 FlowModel 构建可编排的按钮组件 + +在 React 中,我们通常这样渲染一个按钮组件: + +```tsx | pure +import { Button } from 'antd'; + +export default function App() { + return ; +} +``` + +上述代码虽然简单,但属于**静态组件**,无法满足无代码平台对可配置性和编排能力的需求。 + +在 NocoBase 的 FlowEngine 中,我们可以通过 **FlowModel + FlowDefinition** 快速构建支持配置和事件驱动的组件,实现更强大的无代码能力。 + +--- + +## 第一步:使用 FlowModel 渲染组件 + + + +### 🧠 关键概念 + +- `FlowModel` 是 FlowEngine 中的核心组件模型,封装组件逻辑、渲染和配置能力。 +- 每个 UI 组件都可以通过 `FlowModel` 进行实例化并统一管理。 + +### 📌 实现步骤 + +#### 1. 创建自定义模型类 + +```tsx | pure +class MyModel extends FlowModel { + render() { + return +// +// ); +// } + +type BlockGridFlowModelStructure = { + subModels: { + items: BlockFlowModel[]; + }; +}; + +export class BlockGridFlowModel extends FlowModel { + // 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 ( +
+ + +
+ ); + } +} diff --git a/packages/core/client/src/flow/models/CalendarBlockFlowModel.tsx b/packages/core/client/src/flow/models/CalendarBlockFlowModel.tsx new file mode 100644 index 0000000000..e932d1041d --- /dev/null +++ b/packages/core/client/src/flow/models/CalendarBlockFlowModel.tsx @@ -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 ( + + { + this.dispatchEvent('onSelectEvent', { event }); + }} + onDoubleClickEvent={(event) => { + this.dispatchEvent('onDoubleClickEvent', { event }); + }} + events={data.map((item) => { + return { + ...item, + createdAt: item.createdAt ? moment(item.createdAt).toDate() : undefined, + }; + })} + /> + + ); + } +} + +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: ( +
+

Title: {ctx.extra.event.nickname}

+

Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}

+

End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}

+
+ ), + }); + }, + }, + }, +}); + +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: ( +
+

Title: {ctx.extra.event.nickname}

+

Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}

+

End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}

+
+ ), + }); + }, + }, + }, +}); + +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', + }, +}); diff --git a/packages/core/client/src/flow/models/FieldFlowModel.tsx b/packages/core/client/src/flow/models/FieldFlowModel.tsx new file mode 100644 index 0000000000..4d042caf0b --- /dev/null +++ b/packages/core/client/src/flow/models/FieldFlowModel.tsx @@ -0,0 +1,6 @@ +import { Field, FlowModel } from '@nocobase/flow-engine'; + +export class FieldFlowModel extends FlowModel { + field: Field; + fieldPath: string; +} \ No newline at end of file diff --git a/packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx b/packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx new file mode 100644 index 0000000000..1aea3f6bae --- /dev/null +++ b/packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx @@ -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(ref: React.RefObject, 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(); + render() { + return ( + +
+ {/*
*/} + + ); + } +} + +HtmlBlockFlowModel.define({ + title: 'HTML', + group: 'Content', + defaultOptions: { + use: 'HtmlBlockFlowModel', + stepParams: { + default: { + step1: { + html: `

Hello, NocoBase!

+

This is a simple HTML content rendered by FlowModel.

`, + }, + }, + }, + }, +}); + +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); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/PageFlowModel.tsx b/packages/core/client/src/flow/models/PageFlowModel.tsx new file mode 100644 index 0000000000..700356db99 --- /dev/null +++ b/packages/core/client/src/flow/models/PageFlowModel.tsx @@ -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 { + 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: , + }; + }); + } + + renderFirstTab() { + return ; + } + + renderTabs() { + return ( + + this.addTab({ + use: 'PageTabFlowModel', + props: { key: uid(), label: `Tab - ${uid()}` }, + grid: { + use: 'BlockGridFlowModel', + }, + }) + } + > + Add Tab + + } + /> + ); + } + + 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); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/PageTabFlowModel.tsx b/packages/core/client/src/flow/models/PageTabFlowModel.tsx new file mode 100644 index 0000000000..5c5d960e19 --- /dev/null +++ b/packages/core/client/src/flow/models/PageTabFlowModel.tsx @@ -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 ( +
+ +
+ ); + } +} + +PageTabFlowModel.registerFlow({ + key: 'default', + auto: true, + steps: { + step1: { + async handler(ctx, params) { + // model.setProps('label', `Tab123 - ${model.uid}`); + // model.setProps('children', model.renderChildren()); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/action-model.tsx b/packages/core/client/src/flow/models/action-model.tsx new file mode 100644 index 0000000000..db62db895e --- /dev/null +++ b/packages/core/client/src/flow/models/action-model.tsx @@ -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 {this.props.title || 'Untitle'}; + } +} + +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 () => {}, + }); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/form-item-model.tsx b/packages/core/client/src/flow/models/form-item-model.tsx new file mode 100644 index 0000000000..046193f683 --- /dev/null +++ b/packages/core/client/src/flow/models/form-item-model.tsx @@ -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 ( +
+ +
+ ); + } +} + +FormItemModel.registerFlow({ + key: 'default', + auto: true, + steps: { + step1: { + handler(ctx, params) { + const field = ctx.globals.dataSourceManager.getCollectionField(params.fieldPath); + ctx.model.field = field; + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/form-model.tsx b/packages/core/client/src/flow/models/form-model.tsx new file mode 100644 index 0000000000..4e25184ec2 --- /dev/null +++ b/packages/core/client/src/flow/models/form-model.tsx @@ -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 ( +
+ + {this.mapSubModels('fields', (field) => ( + + ))} + + {this.mapSubModels('actions', (action) => ( + + ))} + +
+ +
{JSON.stringify(this.form.values, null, 2)}
+
+
+
+ ); + } + + async openDialog({ filterByTk }) { + return new Promise((resolve) => { + const dialog = FormDialog( + { + footer: null, + title: 'Form Dialog', + }, + (form) => { + return ( +
+ + + + { + await this.resource.save(this.form.values); + dialog.close(); + resolve(this.form.values); // 在 close 之后 resolve + }} + > + Submit + + + +
+ ); + }, + ); + 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', + }, +}); diff --git a/packages/core/client/src/flow/models/index.ts b/packages/core/client/src/flow/models/index.ts new file mode 100644 index 0000000000..d5de58c8ee --- /dev/null +++ b/packages/core/client/src/flow/models/index.ts @@ -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'; +// diff --git a/packages/core/client/src/flow/models/submit-action-model.tsx b/packages/core/client/src/flow/models/submit-action-model.tsx new file mode 100644 index 0000000000..0026b71637 --- /dev/null +++ b/packages/core/client/src/flow/models/submit-action-model.tsx @@ -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 {this.props.title || '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(); + } + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/table-column-model.tsx b/packages/core/client/src/flow/models/table-column-model.tsx new file mode 100644 index 0000000000..2d837aedfc --- /dev/null +++ b/packages/core/client/src/flow/models/table-column-model.tsx @@ -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) => ( + + {value} + { + 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); + }} + /> + + ); + } +} + +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) => ( + + {this.mapSubModels('actions', (action: ActionModel) => ( + + ))} + + ); + } +} + +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; + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/table-model.tsx b/packages/core/client/src/flow/models/table-model.tsx new file mode 100644 index 0000000000..e1dfa7c0e7 --- /dev/null +++ b/packages/core/client/src/flow/models/table-model.tsx @@ -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 { + collection: Collection; + resource: MultiRecordResource; + + getColumns() { + return this.mapSubModels('columns', (column) => column.getColumnProps()).concat({ + key: 'addColumn', + fixed: 'right', + title: ( + + ), + } as any); + } + + render() { + return ( + +
{ + this.resource.setPage(pagination.current); + this.resource.setPageSize(pagination.pageSize); + this.resource.refresh(); + }} + /> + + ); + } +} + +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', + }, +}); diff --git a/packages/core/client/src/index.ts b/packages/core/client/src/index.ts index 7769ecb51a..45fea3703f 100644 --- a/packages/core/client/src/index.ts +++ b/packages/core/client/src/index.ts @@ -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'; diff --git a/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx b/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx new file mode 100644 index 0000000000..e86a069d0c --- /dev/null +++ b/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx @@ -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 ( + + + + + + ); + }, + 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 ( + + ); +}; + +export function getFlowPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) { + return { + type: 'void', + 'x-component': 'FlowPage', + 'x-uid': pageSchemaUid, + }; +} diff --git a/packages/core/client/src/modules/menu/PageMenuItem.tsx b/packages/core/client/src/modules/menu/PageMenuItem.tsx index 819e11592c..c387578fd3 100644 --- a/packages/core/client/src/modules/menu/PageMenuItem.tsx +++ b/packages/core/client/src/modules/menu/PageMenuItem.tsx @@ -107,7 +107,9 @@ export const PageMenuItem = () => { // 同时插入一个对应的 Schema insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName })); }, [createRoute, insertPageSchema, options?.components, options?.scope, parentRoute?.id, t, theme]); - return ; + return ( + + ); }; export function getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) { diff --git a/packages/core/client/src/modules/menu/menuItemInitializer.tsx b/packages/core/client/src/modules/menu/menuItemInitializer.tsx index e7f14115a2..c21731638c 100644 --- a/packages/core/client/src/modules/menu/menuItemInitializer.tsx +++ b/packages/core/client/src/modules/menu/menuItemInitializer.tsx @@ -8,9 +8,10 @@ */ import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { FlowPageMenuItem } from './FlowPageMenuItem'; +import { GroupItem } from './GroupItem'; import { LinkMenuItem } from './LinkMenuItem'; import { PageMenuItem } from './PageMenuItem'; -import { GroupItem } from './GroupItem'; /** * @deprecated @@ -29,7 +30,11 @@ export const menuItemInitializer_deprecated = new SchemaInitializer({ Component: GroupItem, }, { - name: 'page', + name: 'page1', + Component: PageMenuItem, + }, + { + name: 'page2', Component: PageMenuItem, }, { @@ -50,9 +55,13 @@ export const menuItemInitializer = new SchemaInitializer({ Component: GroupItem, }, { - name: 'page', + name: 'page1', Component: PageMenuItem, }, + { + name: 'page2', + Component: FlowPageMenuItem, + }, { name: 'link', Component: LinkMenuItem, diff --git a/packages/core/client/src/nocobase-buildin-plugin/index.tsx b/packages/core/client/src/nocobase-buildin-plugin/index.tsx index 0332a71153..fc79fd8079 100644 --- a/packages/core/client/src/nocobase-buildin-plugin/index.tsx +++ b/packages/core/client/src/nocobase-buildin-plugin/index.tsx @@ -22,6 +22,7 @@ import { BlockSchemaComponentPlugin } from '../block-provider'; import { CollectionPlugin } from '../collection-manager'; import { AppNotFound } from '../common/AppNotFound'; import { RemoteDocumentTitlePlugin } from '../document-title'; +import { PluginFlowEngine } from '../flow'; import { PinnedListPlugin } from '../plugin-manager'; import { PMPlugin } from '../pm'; import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch'; @@ -328,6 +329,7 @@ export class NocoBaseBuildInPlugin extends Plugin { }); } async addPlugins() { + await this.app.pm.add(PluginFlowEngine); await this.app.pm.add(AssociationFilterPlugin); await this.app.pm.add(LocalePlugin, { name: 'builtin-locale' }); await this.app.pm.add(AdminLayoutPlugin, { name: 'admin-layout' }); diff --git a/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts b/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts index ab351cd9cc..4276bc317c 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts +++ b/packages/core/client/src/route-switch/antd/admin-layout/convertRoutesToSchema.ts @@ -10,6 +10,7 @@ export enum NocoBaseDesktopRouteType { group = 'group', page = 'page', + flowPage = 'flowPage', link = 'link', tabs = 'tabs', } diff --git a/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx index 7fa8bd42cb..e23eaab979 100644 --- a/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx +++ b/packages/core/client/src/route-switch/antd/admin-layout/menuItemSettings.tsx @@ -33,6 +33,7 @@ import { import { getPageMenuSchema } from '../../../'; import { SchemaSettings } from '../../../application/schema-settings/SchemaSettings'; import { useInsertPageSchema } from '../../../modules/menu/PageMenuItem'; +import { getFlowPageMenuSchema } from '../../../modules/menu/FlowPageMenuItem'; import { SchemaToolbar } from '../../../schema-settings/GeneralSchemaDesigner'; import { SchemaSettingsItem, @@ -285,6 +286,70 @@ const InsertMenuItems = (props) => { insertPageSchema(getPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName })); }} /> + { + const menuSchemaUid = uid(); + const pageSchemaUid = uid(); + const tabSchemaUid = uid(); + const tabSchemaName = uid(); + const parentId = insertPosition === 'beforeEnd' ? currentRoute?.id : currentRoute?.parentId; + + // 1. 先创建一个路由 + const { data } = await createRoute({ + type: NocoBaseDesktopRouteType.flowPage, + title, + icon, + // 'beforeEnd' 表示的是 Insert inner,此时需要把路由插入到当前路由的内部 + parentId: parentId || undefined, + schemaUid: pageSchemaUid, + menuSchemaUid, + enableTabs: false, + children: [ + { + type: NocoBaseDesktopRouteType.tabs, + schemaUid: tabSchemaUid, + tabSchemaName, + hidden: true, + }, + ], + }); + + if (insertPositionToMethod[insertPosition]) { + // 2. 然后再把路由移动到对应的位置 + await moveRoute({ + sourceId: data?.data?.id, + targetId: currentRoute?.id, + sortField: 'sort', + method: insertPositionToMethod[insertPosition], + }); + } + + // 3. 插入一个对应的 Schema + insertPageSchema(getFlowPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName })); + }} + /> { // 弹窗中显示的内容 expect(within(tooltip).getByText(/name/i)).toBeInTheDocument(); - expect(within(tooltip).getByTitle(/ne/i)).toBeInTheDocument(); + // expect(within(tooltip).getByText(/ne/i)).toBeInTheDocument(); expect(within(tooltip).getByText(/tags \/ title/i)).toBeInTheDocument(); expect(within(tooltip).getByText(/eq/i)).toBeInTheDocument(); expect(within(tooltip).getByText(/^Add condition$/i)).toBeInTheDocument(); diff --git a/packages/core/flow-engine/LICENSE b/packages/core/flow-engine/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/core/flow-engine/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/core/flow-engine/README.md b/packages/core/flow-engine/README.md new file mode 100644 index 0000000000..f09b5197f2 --- /dev/null +++ b/packages/core/flow-engine/README.md @@ -0,0 +1,45 @@ +# @nocobase/flow-engine + +A standalone flow engine for NocoBase, responsible for managing workflows, models, and actions. + +## Overview + +This library provides the core functionalities for defining, registering, and executing flows within a NocoBase application or any compatible JavaScript/TypeScript project. It allows for the creation of models that can have associated flows, and actions that can be reused across different flows and models. + +## Features + +- **Flow Definition**: Define complex workflows with multiple steps. +- **Model Registration**: Register custom models that can interact with flows. +- **Action Registration**: Create and register reusable actions. +- **Event-Driven Flows**: Trigger flows based on specific events. +- **Automatic Flow Execution**: Configure flows to run automatically under certain conditions. +- **Extensible**: Designed to be extended with custom models, actions, and resources. + +## Installation + +```bash +# npm +npm install @nocobase/flow-engine + +# yarn +yarn add @nocobase/flow-engine + +# pnpm +pnpm add @nocobase/flow-engine +``` + +## Basic Usage + +(TODO: Add basic usage examples here) + +## API Reference + +(TODO: Add API reference or link to documentation here) + +## Contributing + +Please refer to the main NocoBase contributing guidelines. + +## License + +Apache-2.0 (Please confirm with the NocoBase project's license) \ No newline at end of file diff --git a/packages/core/flow-engine/package.json b/packages/core/flow-engine/package.json new file mode 100644 index 0000000000..d7795bb30a --- /dev/null +++ b/packages/core/flow-engine/package.json @@ -0,0 +1,29 @@ +{ + "name": "@nocobase/flow-engine", + "version": "1.8.0-beta.4", + "private": false, + "description": "A standalone flow engine for NocoBase, managing workflows, models, and actions.", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "dependencies": { + "@formily/reactive": "2.x", + "@formily/antd-v5": "1.x", + "lodash": "^4.x", + "uid": "^2.0.2", + "@nocobase/sdk": "1.8.0-beta.4" + }, + "peerDependencies": { + "react": ">=18" + }, + "devDependencies": { + "typescript": "^5.x" + }, + "keywords": [ + "nocobase", + "flow", + "engine", + "workflow" + ], + "author": "NocoBase Team", + "license": "AGPL-3.0" +} \ No newline at end of file diff --git a/packages/core/flow-engine/src/components/FlowModelRenderer.tsx b/packages/core/flow-engine/src/components/FlowModelRenderer.tsx new file mode 100644 index 0000000000..d178c8c4e3 --- /dev/null +++ b/packages/core/flow-engine/src/components/FlowModelRenderer.tsx @@ -0,0 +1,204 @@ +/** + * 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. + * + * @example + * // 基本使用 + * + * + * // 显示设置但隐藏删除按钮 + * + * + * // 使用右键菜单模式并隐藏删除按钮 + * + */ + +import { observer } from '@formily/reactive-react'; +import React, { Suspense } from 'react'; +import { useApplyAutoFlows, useFlowExtraContext } from '../hooks'; +import { FlowModel } from '../models'; +import { FlowsContextMenu } from './settings/wrappers/contextual/FlowsContextMenu'; +import { FlowsFloatContextMenu } from './settings/wrappers/contextual/FlowsFloatContextMenu'; +import { Spin } from 'antd'; + +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 + + /** 是否为每个组件独立执行 auto flow,默认 false */ + independentAutoFlowExecution?: boolean; // 默认 false +} + +/** + * 内部组件:带有 useApplyAutoFlows 的渲染器 + */ +const FlowModelRendererWithAutoFlows: React.FC<{ + model: FlowModel; + showFlowSettings: boolean; + flowSettingsVariant: string; + hideRemoveInSettings: boolean; + extraContext?: Record; + independentAutoFlowExecution?: boolean; +}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings, extraContext, independentAutoFlowExecution }) => { + const defaultExtraContext = useFlowExtraContext(); + useApplyAutoFlows(model, extraContext || defaultExtraContext, !independentAutoFlowExecution); + + return ( + + ); +}); + +/** + * 内部组件:不带 useApplyAutoFlows 的渲染器 + */ +const FlowModelRendererWithoutAutoFlows: React.FC<{ + model: FlowModel; + showFlowSettings: boolean; + flowSettingsVariant: string; + hideRemoveInSettings: boolean; +}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings }) => { + return ( + + ); +}); + +/** + * 核心渲染逻辑组件 + */ +const FlowModelRendererCore: React.FC<{ + model: FlowModel; + showFlowSettings: boolean; + flowSettingsVariant: string; + hideRemoveInSettings: boolean; +}> = observer(({ model, showFlowSettings, flowSettingsVariant, hideRemoveInSettings }) => { + // 渲染模型内容 + const modelContent = model.render(); + + // 如果不显示流程设置,直接返回模型内容 + if (!showFlowSettings) { + return modelContent; + } + + // 根据 flowSettingsVariant 包装相应的设置组件 + switch (flowSettingsVariant) { + case 'dropdown': + return {modelContent}; + + case 'contextMenu': + return {modelContent}; + + case 'modal': + // TODO: 实现 modal 模式的流程设置 + console.warn('FlowModelRenderer: modal variant is not implemented yet'); + return modelContent; + + case 'drawer': + // TODO: 实现 drawer 模式的流程设置 + console.warn('FlowModelRenderer: drawer variant is not implemented yet'); + return modelContent; + + default: + console.warn( + `FlowModelRenderer: Unknown flowSettingsVariant '${flowSettingsVariant}', falling back to dropdown`, + ); + return {modelContent}; + } +}); + +/** + * A React component responsible for rendering a FlowModel. + * It delegates the actual rendering logic to the `render` method of the provided model. + * + * @param {FlowModelRendererProps} props - The component's props. + * @param {FlowModel} props.model - The FlowModel instance to render. + * @param {string} props.uid - The unique identifier for the flow model. + * @param {boolean} props.showFlowSettings - Whether to show flow settings entry (buttons, menus, etc.). + * @param {string} props.flowSettingsVariant - The interaction style for flow settings. + * @param {boolean} props.hideRemoveInSettings - Whether to hide remove button in settings. + * @param {boolean} props.skipApplyAutoFlows - Whether to skip applying auto flows. + * @param {any} props.extraContext - Extra context to pass to useApplyAutoFlows when skipApplyAutoFlows is false. + * @param {boolean} props.independentAutoFlowExecution - Whether each component has independent auto flow execution. + * @returns {React.ReactNode | null} The rendered output of the model, or null if the model or its render method is invalid. + */ +export const FlowModelRenderer: React.FC = observer( + ({ + model, + showFlowSettings = false, + flowSettingsVariant = 'dropdown', + hideRemoveInSettings = false, + skipApplyAutoFlows = false, + extraContext, + independentAutoFlowExecution = false, + }) => { + if (!model || typeof model.render !== 'function') { + // 可以选择渲染 null 或者一个错误/提示信息 + console.warn('FlowModelRenderer: Invalid model or render method not found.', model); + return null; + } + + // 根据 skipApplyAutoFlows 选择不同的内部组件 + if (skipApplyAutoFlows) { + return ( + }> + + + ); + } else { + return ( + }> + + + ); + } + }, +); diff --git a/packages/core/flow-engine/src/components/index.ts b/packages/core/flow-engine/src/components/index.ts new file mode 100644 index 0000000000..4a13de2162 --- /dev/null +++ b/packages/core/flow-engine/src/components/index.ts @@ -0,0 +1,12 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './FlowModelRenderer'; +export * from './settings'; +export * from './subModel'; \ No newline at end of file diff --git a/packages/core/flow-engine/src/components/settings/AddAction.tsx b/packages/core/flow-engine/src/components/settings/AddAction.tsx new file mode 100644 index 0000000000..30c5738777 --- /dev/null +++ b/packages/core/flow-engine/src/components/settings/AddAction.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import { Alert, Button, Select, Space } from 'antd'; +import { useFlowModel } from '../../hooks'; +import { BlockModel } from '../../models'; +import { observer } from '@formily/react'; +import { PlusOutlined } from '@ant-design/icons'; + +// 创建两个组件版本,一个使用props传递的model,一个使用hook获取model +interface ModelProvidedProps { + model: BlockModel; + expandAll?: boolean; // 是否展开所有Collapse +} + +interface ModelByIdProps { + uid: string; + modelClassName: string; + expandAll?: boolean; // 是否展开所有Collapse +} + +type AddActionProps = ModelProvidedProps | ModelByIdProps; + +// 判断是否是通过ID获取模型的props +const isModelByIdProps = (props: AddActionProps): props is ModelByIdProps => { + return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName); +}; + +/** + * AddAction 组件 - 添加 Block 模型的 Action + * 支持两种使用方式: + * 1. 直接提供model: + * 2. 通过uid和modelClassName获取model: + */ +const AddAction: React.FC = (props) => { + if (isModelByIdProps(props)) { + return ; + } else { + return ; + } +}; + +// 使用传入的model +const AddActionWithModel: React.FC = observer(({ model }) => { + if (!model) { + return ; + } + + return ; +}); + +// 通过useModelById hook获取model +const AddActionWithModelById: React.FC = observer(({ uid, modelClassName }) => { + const model = useFlowModel(uid, modelClassName) as BlockModel; + + if (!model) { + return ; + } + + return ; +}); + +// 添加新Action的表单组件 +const AddActionForm: React.FC<{ model: BlockModel }> = observer(({ model }) => { + const [selectedActionType, setSelectedActionType] = useState(null); + const ModelClass = model.constructor as typeof BlockModel; + const supportedActions = ModelClass.supportedActions; + + // 处理添加Action + const handleAdd = () => { + if (!selectedActionType) return; + + // 找到选中的Action类型 + const actionConfig = supportedActions.find((action) => action.type.name === selectedActionType); + + if (actionConfig) { + // 创建新的Action实例 + const newActionUid = `${model.uid}_action_${Date.now()}`; + const newAction = new actionConfig.type({ + uid: newActionUid, + stepParams: model.stepParams, + blockModel: model, + }); + newAction.setFlowEngine(model.flowEngine); + + // 添加到模型中 + model.addAction(newAction); + + // 重置选择 + setSelectedActionType(null); + } + }; + + return ( + + + ); + case 'InputNumber': + return ( + + ); + case 'Switch': + return ; + case 'Input.TextArea': + return ( + + ); + default: + return ( + + ); + } + }; + + return ( + + {renderField()} + + ); + }); + }); + }; + + return ( +
+ {renderFormFields()} + + ); +}); + +export { FlowSettings }; diff --git a/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettings.tsx b/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettings.tsx new file mode 100644 index 0000000000..72ec885359 --- /dev/null +++ b/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettings.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { Alert } from 'antd'; +import { useFlowModel } from '../../../../hooks'; +import { observer } from '@formily/react'; +import FlowsSettingsContent from './FlowsSettingsContent'; + +// 简单的流程设置组件接口 +interface ModelProvidedProps { + model: any; + expandAll?: boolean; // 是否展开所有Collapse,默认为false + children?: React.ReactNode; // 子组件,如果提供则作为wrapper模式 +} + +interface ModelByIdProps { + uid: string; + modelClassName: string; + expandAll?: boolean; // 是否展开所有Collapse,默认为false + children?: React.ReactNode; // 子组件,如果提供则作为wrapper模式 +} + +type FlowsSettingsProps = ModelProvidedProps | ModelByIdProps; + +// 判断是否是通过ID获取模型的props +const isModelByIdProps = (props: FlowsSettingsProps): props is ModelByIdProps => { + return 'uid' in props && 'modelClassName' in props && Boolean(props.uid) && Boolean(props.modelClassName); +}; + +/** + * FlowsSettings组件 - 简单的流程配置界面 + * + * 功能特性: + * - 流程配置界面 + * - Wrapper 模式支持 + * + * 支持两种使用方式: + * 1. 直接提供model: + * 2. 通过uid和modelClassName获取model: + * + * 支持两种模式: + * 1. 独立设置界面: + * 2. Wrapper模式: {children} + * + * @param props.expandAll 是否展开所有Collapse,默认为false + * @param props.children 子组件,如果提供则作为wrapper模式 + */ +const FlowsSettings: React.FC = (props) => { + if (isModelByIdProps(props)) { + return ; + } else { + return ; + } +}; + +// 使用传入的model +const FlowsSettingsWithModel: React.FC = observer(({ model, expandAll = false, children }) => { + if (!model) { + return ; + } + + const settingsContent = ; + + // 如果有children,作为wrapper模式 + if (children) { + return ( +
+ {settingsContent} + {children} +
+ ); + } + + // 如果没有children,返回设置内容 + return settingsContent; +}); + +// 通过useModelById hook获取model +const FlowsSettingsWithModelById: React.FC = observer( + ({ uid, modelClassName, expandAll = false, children }) => { + const model = useFlowModel(uid, modelClassName); + + if (!model) { + return ; + } + + const settingsContent = ; + + // 如果有children,作为wrapper模式 + if (children) { + return ( +
+ {settingsContent} + {children} +
+ ); + } + + // 如果没有children,返回设置内容 + return settingsContent; + }, +); + +export { FlowsSettings }; diff --git a/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx b/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx new file mode 100644 index 0000000000..e51f53a8ef --- /dev/null +++ b/packages/core/flow-engine/src/components/settings/wrappers/embedded/FlowsSettingsContent.tsx @@ -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 React from 'react'; +import { Card, Empty, Collapse } from 'antd'; +import { ActionStepDefinition } from '../../../../types'; +import { FlowModel } from '../../../../models'; +import { observer } from '@formily/react'; +import { FlowSettings } from './FlowSettings'; + +const { Panel } = Collapse; + +// 提取核心渲染逻辑到一个共享组件 +interface FlowsSettingsContentProps { + model: FlowModel; + expandAll?: boolean; +} + +// 默认使用 Collapse 组件渲染多个流程设置 +const FlowsSettingsContent: React.FC = observer(({ model, expandAll = false }) => { + const ModelClass = model.constructor as typeof FlowModel; + const flows = ModelClass.getFlows(); + + const filterFlows = Array.from(flows.values()).filter((flow) => { + const configurableSteps = Object.entries(flow.steps) + .map(([stepKey, stepDefinition]) => { + const actionStep = stepDefinition as ActionStepDefinition; + + // 如果步骤设置了 hideInSettings: true,则跳过此步骤 + if (actionStep.hideInSettings) { + return null; + } + + // 从step获取uiSchema(如果存在) + const stepUiSchema = actionStep.uiSchema || {}; + + // 如果step使用了action,也获取action的uiSchema + let actionUiSchema = {}; + if (actionStep.use) { + const action = model.flowEngine?.getAction?.(actionStep.use); + if (action && action.uiSchema) { + actionUiSchema = action.uiSchema; + } + } + + // 合并uiSchema,确保step的uiSchema优先级更高 + // 先复制action的uiSchema,然后用step的uiSchema覆盖相同的字段 + const mergedUiSchema = { ...actionUiSchema }; + + // 将stepUiSchema中的字段合并到mergedUiSchema + Object.entries(stepUiSchema).forEach(([fieldKey, schema]) => { + if (mergedUiSchema[fieldKey]) { + // 如果字段已存在,则合并schema对象,保持step中的属性优先级更高 + mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema }; + } else { + // 如果字段不存在,则直接添加 + mergedUiSchema[fieldKey] = schema; + } + }); + + // 如果没有可配置的UI Schema,返回null + if (Object.keys(mergedUiSchema).length === 0) { + return null; + } + + return { stepKey, step: actionStep, uiSchema: mergedUiSchema }; + }) + .filter(Boolean); + return configurableSteps.length > 0; + }); + + const flowKeys = filterFlows.map((flow) => flow.key); + if (flowKeys.length === 0) { + return ; + } + + // 如果expandAll为true,则默认展开所有面板 + const defaultActiveKey = expandAll ? flowKeys : undefined; + + return ( + + + {flowKeys.map((flowKey) => ( + + + + ))} + + + ); +}); + +export default FlowsSettingsContent; diff --git a/packages/core/flow-engine/src/components/settings/wrappers/embedded/index.ts b/packages/core/flow-engine/src/components/settings/wrappers/embedded/index.ts new file mode 100644 index 0000000000..03d347be21 --- /dev/null +++ b/packages/core/flow-engine/src/components/settings/wrappers/embedded/index.ts @@ -0,0 +1,11 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './FlowSettings'; +export * from './FlowsSettings'; diff --git a/packages/core/flow-engine/src/components/settings/wrappers/index.ts b/packages/core/flow-engine/src/components/settings/wrappers/index.ts new file mode 100644 index 0000000000..1ff8968333 --- /dev/null +++ b/packages/core/flow-engine/src/components/settings/wrappers/index.ts @@ -0,0 +1,5 @@ +// 嵌入式wrapper组件 - 实时保存配置 +export * from './embedded'; + +// 上下文菜单wrapper组件 - 确认后保存配置 +export * from './contextual'; diff --git a/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx new file mode 100644 index 0000000000..720aa65abe --- /dev/null +++ b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx @@ -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 { observer } from '@formily/reactive-react'; +import React, { useMemo } from 'react'; +import { AddSubModelButton, AddSubModelButtonProps } from './AddSubModelButton'; +import { FlowModel } from '../../models/flowModel'; + +interface AddActionButtonProps extends Omit { + subModelKey?: string; + subModelType?: 'object' | 'array'; +} + +/** + * 专门用于添加动作模型的按钮组件 + * + * @example + * ```tsx + * + * ``` + */ +export const AddActionButton: React.FC = observer(({ + ParentModelClass='ActionFlowModel', + subModelKey = 'actions', + children = 'Add action', + subModelType = 'array', + ...props +}) => { + const items = useMemo<{ + key: string; + label: string; + icon?: React.ReactNode; + item: typeof FlowModel; + unique?: boolean; + use: string; + }[]>(() => { + const blockClasses = props.model.flowEngine.filterModelClassByParent(ParentModelClass); + const registeredBlocks = []; + for (const [className, ModelClass] of blockClasses) { + if (ModelClass.meta) { + const item = { + key: className, + label: ModelClass.meta?.title, + icon: ModelClass.meta?.icon, + item: ModelClass, + use: className, + // unique: ModelClass.meta?.uniqueSub, + // added: null, + }; + registeredBlocks.push(item); + } + } + return registeredBlocks; + }, [props.model, ParentModelClass]); + + return ( + + {children} + + ); +}); + +AddActionButton.displayName = 'AddActionButton'; diff --git a/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx b/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx new file mode 100644 index 0000000000..d851f29721 --- /dev/null +++ b/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx @@ -0,0 +1,77 @@ +/** + * 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 { observer } from '@formily/reactive-react'; +import React, { useMemo } from 'react'; +import { AddSubModelButton, AddSubModelButtonProps, AddSubModelItem } from './AddSubModelButton'; +import { FlowModel } from '../../models/flowModel'; + +interface AddBlockButtonProps extends Omit { + subModelKey?: string; + subModelType?: 'object' | 'array'; +} + +/** + * 专门用于添加块模型的按钮组件 + * + * @example + * ```tsx + * + * ``` + */ +export const AddBlockButton: React.FC = observer(({ + ParentModelClass='BlockFlowModel', + subModelKey = 'blocks', + children = 'Add block', + subModelType = 'array', + ...props +}) => { + const items = useMemo<{ + key: string; + label: string; + icon?: React.ReactNode; + item: typeof FlowModel; + use: string; + unique?: boolean; + }[]>(() => { + const blockClasses = props.model.flowEngine.filterModelClassByParent(ParentModelClass); + const registeredBlocks = []; + for (const [className, ModelClass] of blockClasses) { + if (ModelClass.meta) { + registeredBlocks.push({ + key: className, + label: ModelClass.meta?.title, + icon: ModelClass.meta?.icon, + item: ModelClass, + use: className, + // unique: ModelClass.meta?.uniqueSub, + // added: null, + }); + } + } + return registeredBlocks; + }, [props.model, ParentModelClass]); + + return ( + + {children} + + ); +}); + +AddBlockButton.displayName = 'AddBlockButton'; diff --git a/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx new file mode 100644 index 0000000000..4276a68cae --- /dev/null +++ b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx @@ -0,0 +1,104 @@ +/** + * 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 { observer } from '@formily/reactive-react'; +import React, { useMemo } from 'react'; +import { AddSubModelButton, AddSubModelButtonProps, AddSubModelItem } from './AddSubModelButton'; +import { Collection } from '../../data-source'; + + +interface AddFieldButtonProps extends Omit { + subModelKey?: string; + subModelType?: 'object' | 'array'; + collection: Collection; +} + +/** + * 专门用于添加字段模型的按钮组件 + * + * @example + * ```tsx + * + * ``` + */ +export const AddFieldButton: React.FC = observer(({ + ParentModelClass = 'TableColumnModel', + subModelKey = 'fields', + children = 'Add field', + subModelType = 'array', + ...props +}) => { + const fields = props.collection.getFields(); + const items = useMemo(() => { + const fieldClasses = Array.from(props.model.flowEngine.filterModelClassByParent(ParentModelClass).values()).filter(c => !!c.meta) + ?.sort((a, b) => (a.meta?.sort || 0) - (b.meta?.sort || 0)); + const allFields = []; + + const defaultFieldClasses = fieldClasses.filter( + fieldClass => !fieldClass.meta?.supportedInterfaces && !fieldClass.meta?.supportedInterfaceGroups + ); + + for (const field of fields) { + const fieldInterfaceName = field.options?.interface; + if (fieldInterfaceName) { + const fieldClass = fieldClasses.find(fieldClass => fieldClass.meta?.supportedInterfaces?.includes(fieldInterfaceName)); + if (fieldClass) { + allFields.push({ + key: field.name, + label: field.title, + item: fieldClass, + use: fieldClass.name, + field, + props: { + dataIndex: field.name, + title: field.title + }, + }); + } else if (defaultFieldClasses) { + allFields.push({ + key: field.name, + label: field.title, + item: defaultFieldClasses[0], + use: defaultFieldClasses[0]?.name, + field, + props: { + dataIndex: field.name, + title: field.title + }, + }); + } + } + } + return allFields; + }, [props.model, ParentModelClass, fields]); + + const afterAdd = (subModel, item) => { + const field = item.field; + subModel.field = field; + subModel.fieldPath = `${field.collection.dataSource.name}.${field.collection.name}.${field.name}`; + }; + + return ( + + {children} + + ); +}); + +AddFieldButton.displayName = 'AddFieldButton'; diff --git a/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx b/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx new file mode 100644 index 0000000000..a19896b117 --- /dev/null +++ b/packages/core/flow-engine/src/components/subModel/AddSubModelButton.tsx @@ -0,0 +1,216 @@ +/** + * 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 { observer } from '@formily/reactive-react'; +import { Button, ButtonProps, Dropdown, MenuProps, Switch } from 'antd'; +import { DownOutlined } from '@ant-design/icons'; +import React, { useMemo } from 'react'; +import { FlowModel } from '../../models'; +import { CreateModelOptions, ModelConstructor } from '../../types'; +import { generateUid } from '../../utils'; +import _ from 'lodash'; + +export interface AddSubModelItem { + key: string; + label: string; + icon?: React.ReactNode; + item: typeof FlowModel; + use: string; + props?: Record; + // unique?: boolean; + // added?: FlowModel; +} + +export interface AddSubModelButtonProps extends Omit { + /** + * 父模型类名,用于确定支持的块类型 + */ + ParentModelClass?: string | ModelConstructor; + + /** + * 父模型实例 + */ + model: FlowModel; + + /** + * 子模型类型列表 + */ + items: AddSubModelItem[]; + + /** + * 子模型类型:'object' 表示单个子模型,'array' 表示子模型数组 + */ + subModelType: 'object' | 'array'; + + /** + * 子模型在父模型中的键名 + */ + subModelKey: string; + + /** + * 点击后的回调函数 + */ + onAfterAdd?: (subModel: FlowModel, item: AddSubModelItem) => void; + + /** + * 按钮文本,默认为 "Add" + */ + children?: React.ReactNode; +} + +/** + * 为 FlowModel 实例添加子模型的通用按钮组件 + * + * 功能特性: + * - 当 items 数组为空或只有一个选项时,显示普通按钮,点击直接添加 + * - 当 items 数组有多个选项时,显示带下拉箭头的按钮,鼠标悬停显示选项菜单 + * - 支持自定义图标和标签 + * - 自动处理子模型的创建、保存和回调 + * + */ +export const AddSubModelButton: React.FC = observer(({ + model, + items, + ParentModelClass, + subModelType, + subModelKey, + onAfterAdd, + children = 'Add', + ...buttonProps +}) => { + const [_update, forceUpdate] = React.useState(0); + const buildSubModelParams = React.useCallback((info: { + key: string; + }) => { + const blockType = items.find(type => type.key === info.key); + + if (!blockType) { + throw new Error(`Unknown block type: ${info.key}`); + } + + if (blockType.item.meta?.defaultOptions) { + return { ..._.cloneDeep(blockType.item.meta?.defaultOptions), use: blockType.use, props: blockType.props }; + } else { + return { + use: blockType.use, + props: blockType.props, + } + } + }, [items]); + + + const handleAddSubModel = async (selectedItem?: any) => { + try { + const item = selectedItem || items[0]; + const key = item?.key || generateUid(); + + const subModelOptions = buildSubModelParams({ key }); + + let subModel: FlowModel = model.flowEngine.createModel({...subModelOptions, subKey: subModelKey, subType: 'array'}); + + try { + await subModel.configureRequiredSteps(); + if (onAfterAdd) { + onAfterAdd(subModel, item); + } + if (subModelType === 'array') { + subModel = model.addSubModel(subModelKey, subModel); + } else { + subModel = model.setSubModel(subModelKey, subModel); + } + await subModel.save(); + + // 如果是 unique 项目,标记为已添加 + // if (item?.unique) { + // item.added = subModel; + // } + forceUpdate(prev => prev + 1); + } catch (error) { + console.error('Failed to add sub model:', error); + await subModel.destroy(); + } + } catch (error) { + console.error('Failed to add sub model:', error); + } + }; + + const handleClick = async () => { + // 如果没有提供 items 或只有一个选项,直接添加 + if (items.length <= 1) { + await handleAddSubModel(); + } + // 如果有多个选项,点击事件由 Dropdown 处理 + }; + + // 如果有多个选项,显示下拉菜单 + if (items.length > 1) { + const menuItems: MenuProps['items'] = items.map((item) => ({ + key: item.key, + label: ( +
+
+ {item.icon} + {item.label} +
+ {/* {item.unique && ( + { + if (checked) { + handleAddSubModel(item); + } else { + item.added?.destroy(); + item.added = null; + forceUpdate(prev => prev + 1); + } + }} + onClick={(checked, e) => { + e.stopPropagation(); + }} + /> + )} */} +
+ ), + disabled: false, // 不禁用整个菜单项,让开关可以操作 + onClick: () => { + // if (!item.unique) { + // handleAddSubModel(item); + // } + handleAddSubModel(item); + }, + })); + + return ( + + + + ); + } + + return ( + + ); +}); + +AddSubModelButton.displayName = 'AddSubModelButton'; diff --git a/packages/core/flow-engine/src/components/subModel/index.ts b/packages/core/flow-engine/src/components/subModel/index.ts new file mode 100644 index 0000000000..3eeecd2a4c --- /dev/null +++ b/packages/core/flow-engine/src/components/subModel/index.ts @@ -0,0 +1,13 @@ +/** + * 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 './AddSubModelButton'; +export * from './AddBlockButton'; +export * from './AddActionButton'; +export * from './AddFieldButton'; diff --git a/packages/core/flow-engine/src/data-source/index.ts b/packages/core/flow-engine/src/data-source/index.ts new file mode 100644 index 0000000000..beec3eb76d --- /dev/null +++ b/packages/core/flow-engine/src/data-source/index.ts @@ -0,0 +1,333 @@ +/** + * 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 { Schema } from '@formily/json-schema'; +import { observable } from '@formily/reactive'; + +export interface DataSourceOptions extends Record { + name: string; + use?: typeof DataSource; + displayName?: string; + description?: string; + [key: string]: any; +} + +export class DataSourceManager { + dataSources: Map; + + constructor() { + this.dataSources = observable.shallow>(new Map()); + } + + addDataSource(ds: DataSource | DataSourceOptions) { + if (this.dataSources.has(ds.name)) { + throw new Error(`DataSource with name ${ds.name} already exists`); + } + if (ds instanceof DataSource) { + this.dataSources.set(ds.name, ds); + } else { + const clz = ds.use || DataSource; + this.dataSources.set(ds.name, new clz(ds)); + } + } + + removeDataSource(name: string) { + this.dataSources.delete(name); + } + + clearDataSources() { + this.dataSources.clear(); + } + + getDataSources(): DataSource[] { + return Array.from(this.dataSources.values()); + } + + getDataSource(name: string): DataSource | undefined { + return this.dataSources.get(name); + } + + getCollection(dataSourceName: string, collectionName: string): Collection | undefined { + const ds = this.getDataSource(dataSourceName); + if (!ds) return undefined; + return ds.collectionManager.getCollection(collectionName); + } + + getCollectionField(fieldPathWithDataSource: string) { + const [dataSourceName, ...otherKeys] = fieldPathWithDataSource.split('.'); + const ds = this.getDataSource(dataSourceName); + if (!ds) return undefined; + return ds.getCollectionField(otherKeys.join('.')); + } +} + +export class DataSource { + collectionManager: CollectionManager; + options: Record; + + constructor(options: Record = {}) { + this.options = observable({ ...options }); + this.collectionManager = new CollectionManager(this); + } + + get name() { + return this.options.name; + } + + getCollections(): Collection[] { + return this.collectionManager.getCollections(); + } + + getCollection(name: string): Collection | undefined { + return this.collectionManager.getCollection(name); + } + + addCollection(collection: Collection | CollectionOptions) { + return this.collectionManager.addCollection(collection); + } + + updateCollection(newOptions: CollectionOptions) { + return this.collectionManager.updateCollection(newOptions); + } + + removeCollection(name: string) { + return this.collectionManager.removeCollection(name); + } + + clearCollections() { + this.collectionManager.clearCollections(); + } + + setOptions(newOptions: any = {}) { + Object.keys(this.options).forEach((key) => delete this.options[key]); + Object.assign(this.options, newOptions); + } + + getCollectionField(fieldPath: string) { + const [collectionName, ...otherKeys] = fieldPath.split('.'); + const fieldName = otherKeys.join('.'); + const collection = this.getCollection(collectionName); + if (!collection) { + throw new Error(`Collection ${collectionName} not found in data source ${this.name}`); + } + const field = collection.getField(fieldName); + if (!field) { + throw new Error(`Field ${fieldName} not found in collection ${collectionName}`); + } + return field; + } + + refresh() { + // 刷新数据源 + } +} + +export interface CollectionOptions { + name: string; + title?: string; + inherits?: string[]; + [key: string]: any; +} + +export class CollectionManager { + collections: Map; + + constructor(protected dataSource: DataSource) { + this.collections = observable.shallow>(new Map()); + } + + addCollection(collection: Collection | CollectionOptions) { + let col: Collection; + if (collection instanceof Collection) { + col = collection; + } else { + col = new Collection(collection); + } + col.setDataSource(this.dataSource); + col.initInherits(); + this.collections.set(col.name, col); + } + + removeCollection(name: string) { + this.collections.delete(name); + } + + updateCollection(newOptions: CollectionOptions) { + const collection = this.getCollection(newOptions.name); + if (!collection) { + throw new Error(`Collection ${newOptions.name} not found`); + } + collection.setOptions(newOptions); + } + + getCollection(name: string): Collection | undefined { + return this.collections.get(name); + } + + getCollections(): Collection[] { + return Array.from(this.collections.values()); + } + + clearCollections() { + this.collections.clear(); + } +} + +// Collection 负责管理自己的 Field +export class Collection { + fields: Map; + options: Record; + inherits: Map; + dataSource: DataSource; + + constructor(options: Record = {}) { + this.options = observable({ ...options }); + this.fields = observable.shallow>(new Map()); + this.inherits = observable.shallow>(new Map()); + this.setFields(options.fields || []); + } + + get collectionManager() { + return this.dataSource.collectionManager; + } + + get name() { + return this.options.name; + } + + get title() { + return this.options.title || this.name; + } + + initInherits() { + this.inherits.clear(); + for (const inherit of this.options.inherits || []) { + const collection = this.collectionManager.getCollection(inherit); + if (!collection) { + throw new Error(`Collection ${inherit} not found`); + } + this.inherits.set(inherit, collection); + } + } + + setDataSource(dataSource: DataSource) { + this.dataSource = dataSource; + } + + setOptions(newOptions: any = {}) { + Object.keys(this.options).forEach((key) => delete this.options[key]); + Object.assign(this.options, newOptions); + this.initInherits(); + } + + getFields(): Field[] { + // 合并自身 fields 和所有 inherits 的 fields,后者优先被覆盖 + const fieldMap = new Map(); + for (const inherit of this.inherits.values()) { + if (inherit && typeof inherit.getFields === 'function') { + for (const field of inherit.getFields()) { + fieldMap.set(field.name, field); + } + } + } + // 自身 fields 覆盖同名 + for (const [name, field] of this.fields.entries()) { + fieldMap.set(name, field); + } + return Array.from(fieldMap.values()); + } + + mapFields(callback: (field: Field) => any): any[] { + return this.getFields().map(callback); + } + + setFields(fields: Field[] | Record[]) { + this.fields.clear(); + for (const field of fields) { + this.addField(field); + } + } + + getField(fieldName: string): Field | undefined { + return this.fields.get(fieldName); + } + + addField(field: Field | Record) { + if (field.name && this.fields.has(field.name)) { + throw new Error(`Field with name ${field.name} already exists in collection ${this.name}`); + } + if (field instanceof Field) { + field.setCollection(this); + this.fields.set(field.name, field); + } else { + const newField = new Field(field); + newField.setCollection(this); + this.fields.set(newField.name, newField); + } + } + + removeField(fieldName: string) { + return this.fields.delete(fieldName); + } + + clearFields() { + return this.fields.clear(); + } + + refresh() { + // 刷新集合 + } +} + +export class Field { + options: Record; + collection: Collection; + + constructor(options: Record) { + this.options = observable({ ...options }); + } + + setOptions(newOptions: any = {}) { + Object.keys(this.options).forEach((key) => delete this.options[key]); + Object.assign(this.options, newOptions); + } + + setCollection(collection: Collection) { + this.collection = collection; + } + + get name() { + return this.options.name; + } + + get type() { + return this.options.type; + } + + get title() { + return Schema.compile(this.options?.title || this.options?.uiSchema?.title || this.options.name, { + t: (text) => text, + }); + } + + set title(value: string) { + this.options.title = value; + } + + getFields(): Field[] { + if (!this.options.target) { + return []; + } + const targetCollection = this.collection.collectionManager.getCollection(this.options.target); + if (!targetCollection) { + throw new Error(`Target collection ${this.options.target} not found for field ${this.name}`); + } + return targetCollection.getFields(); + } +} diff --git a/packages/core/flow-engine/src/flowEngine.ts b/packages/core/flow-engine/src/flowEngine.ts new file mode 100644 index 0000000000..38be61723a --- /dev/null +++ b/packages/core/flow-engine/src/flowEngine.ts @@ -0,0 +1,287 @@ +/** + * 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 { observable } from '@formily/reactive'; +import { FlowSettings } from './flowSettings'; +import { FlowModel } from './models'; +import { + ActionDefinition, + ActionOptions, + CreateModelOptions, + FlowDefinition, + IFlowModelRepository, + ModelConstructor, +} from './types'; +import { isInheritedFrom } from './utils'; + +interface ApplyFlowCacheEntry { + status: 'pending' | 'resolved' | 'rejected'; + promise: Promise; + data?: any; + error?: any; +} + +export class FlowEngine { + /** @private Stores registered action definitions. */ + private actions: Map = new Map(); + /** @private Stores registered model constructors. */ + private modelClasses: Map = observable.shallow(new Map()); + /** @private Stores created model instances. */ + private modelInstances: Map = new Map(); + /** @public Stores flow settings including components and scopes for formily settings. */ + public flowSettings: FlowSettings = new FlowSettings(); + context: Record = {}; + private modelRepository: IFlowModelRepository | null = null; + private _applyFlowCache = new Map(); + + setModelRepository(modelRepository: IFlowModelRepository) { + if (this.modelRepository) { + console.warn('FlowEngine: Model repository is already set and will be overwritten.'); + } + this.modelRepository = modelRepository; + } + + setContext(context: any) { + this.context = context; + } + + getContext() { + return this.context; + } + + get applyFlowCache() { + return this._applyFlowCache; + } + + /** + * 注册一个 Action。支持泛型以确保正确的模型类型推导。 + * Action 是流程中的可复用操作单元。 + * @template TModel 具体的FlowModel子类类型 + * @param {string | ActionDefinition} nameOrDefinition Action 名称或 ActionDefinition 对象。 + * 如果为字符串,则为 Action 名称,需要配合 options 参数。 + * 如果为对象,则为完整的 ActionDefinition。 + * @param {ActionOptions} [options] 当第一个参数为 Action 名称时,此参数为 Action 的选项。 + * @returns {void} + * @example + * // 方式一: 传入名称和选项 + * flowEngine.registerAction('myAction', { handler: async (ctx, params) => { ... } }); + * // 方式二: 传入 ActionDefinition 对象 + * flowEngine.registerAction({ name: 'myAction', handler: async (ctx, params) => { ... } }); + */ + public registerAction( + nameOrDefinition: string | ActionDefinition, + options?: ActionOptions, + ): void { + let definition: ActionDefinition; + + if (typeof nameOrDefinition === 'string' && options) { + definition = { + ...options, + name: nameOrDefinition, + }; + } else if (typeof nameOrDefinition === 'object') { + definition = nameOrDefinition as ActionDefinition; + } else { + throw new Error('Invalid arguments for registerAction'); + } + + if (this.actions.has(definition.name)) { + console.warn(`FlowEngine: Action with name '${definition.name}' is already registered and will be overwritten.`); + } + this.actions.set(definition.name, definition as ActionDefinition); + } + + /** + * 获取已注册的 Action 定义。 + * @template TModel 具体的FlowModel子类类型 + * @param {string} name Action 名称。 + * @returns {ActionDefinition | undefined} Action 定义,如果未找到则返回 undefined。 + */ + public getAction(name: string): ActionDefinition | undefined { + return this.actions.get(name) as ActionDefinition | undefined; + } + + private registerModel(name: string, modelClass: ModelConstructor): void { + if (this.modelClasses.has(name)) { + console.warn(`FlowEngine: Model class with name '${name}' is already registered and will be overwritten.`); + } + this.modelClasses.set(name, modelClass); + } + + /** + * 注册 Model 类。 + * @param {Record} models 要注册的 Model 类映射表,键为 Model 名称,值为 Model 构造函数。 + * @returns {void} + * @example + * flowEngine.registerModels({ + * 'UserModel': UserModel, + * 'OrderModel': OrderModel, + * 'ProductModel': ProductModel + * }); + */ + public registerModels(models: Record) { + for (const [name, modelClass] of Object.entries(models)) { + this.registerModel(name, modelClass); + } + } + + getModelClasses() { + return this.modelClasses; + } + + /** + * 获取已注册的 Model 类 (构造函数)。 + * @param {string} name Model 类名称。 + * @returns {ModelConstructor | undefined} Model 构造函数,如果未找到则返回 undefined。 + */ + public getModelClass(name: string): ModelConstructor | undefined { + return this.modelClasses.get(name); + } + + /** + * 创建并注册一个 Model 实例。 + * 如果具有相同 UID 的实例已存在,则返回现有实例。 + * @template T FlowModel 的子类型,默认为 FlowModel。 + * @param {CreateModelOptions} options 创建模型的选项 + * @returns {T} 创建的 Model 实例。 + */ + public createModel(options: CreateModelOptions): T { + const { parentId, uid, use: modelClassName, subModels } = options; + const ModelClass = typeof modelClassName === 'string' ? this.getModelClass(modelClassName) : modelClassName; + + if (!ModelClass) { + throw new Error(`Model class '${modelClassName}' not found. Please register it first.`); + } + + if (uid && this.modelInstances.has(uid)) { + return this.modelInstances.get(uid) as T; + } + const modelInstance = new (ModelClass as ModelConstructor)({ ...options, flowEngine: this } as any); + + modelInstance.onInit(options); + + if (parentId && this.modelInstances.has(parentId)) { + modelInstance.setParent(this.modelInstances.get(parentId)); + } + + this.modelInstances.set(modelInstance.uid, modelInstance); + + return modelInstance; + } + + /** + * 根据 UID 获取 Model 实例。 + * @template T FlowModel 的子类型,默认为 FlowModel。 + * @param {string} uid Model 实例的唯一标识符。 + * @returns {T | undefined} Model 实例,如果未找到则返回 undefined。 + */ + public getModel(uid: string): T | undefined { + return this.modelInstances.get(uid) as T | undefined; + } + + /** + * 销毁并移除一个 Model 实例。 + * @param {string} uid 要销毁的 Model 实例的唯一标识符。 + * @returns {boolean} 如果成功销毁则返回 true,否则返回 false (例如,实例不存在)。 + */ + public removeModel(uid: string): boolean { + if (this.modelInstances.has(uid)) { + return this.modelInstances.delete(uid); + } + return false; + } + + private ensureModelRepository(): boolean { + if (!this.modelRepository) { + // 不抛错,直接返回 false + return false; + } + return true; + } + + async loadModel(uid: string): Promise { + if (!this.ensureModelRepository()) return; + const data = await this.modelRepository.load(uid); + return data?.uid ? this.createModel(data as any) : null; + } + + async loadOrCreateModel(options): Promise { + if (!this.ensureModelRepository()) return; + const data = await this.modelRepository.load(options.uid); + if (data?.uid) { + return this.createModel(data as any); + } else { + const model = this.createModel(options); + await model.save(); + return model; + } + } + + async saveModel(model: T) { + if (!this.ensureModelRepository()) return; + return await this.modelRepository.save(model); + } + + async destroyModel(uid: string) { + if (this.ensureModelRepository()) { + await this.modelRepository.destroy(uid); + } + return this.removeModel(uid); + } + + /** + * 注册一个流程 (Flow)。支持泛型以确保正确的模型类型推导。 + * 流程是一系列步骤的定义,可以由事件触发或手动应用。 + * @template TModel 具体的FlowModel子类类型 + * @param {string} modelClassName 模型类名称。 + * @param {FlowDefinition} flowDefinition 流程定义。 + * @returns {void} + */ + public registerFlow( + modelClassName: string, + flowDefinition: FlowDefinition, + ): void { + const ModelClass = this.getModelClass(modelClassName); + + // 检查ModelClass是否存在 + if (!ModelClass) { + console.warn( + `FlowEngine: Model class '${modelClassName}' not found. Flow '${flowDefinition.key}' will not be registered.`, + ); + return; + } + + if (typeof (ModelClass as any).registerFlow !== 'function') { + console.warn( + `FlowEngine: Model class '${modelClassName}' does not have a static registerFlow method. Flow '${flowDefinition.key}' will not be registered.`, + ); + return; + } + + (ModelClass as any).registerFlow(flowDefinition); + } + + /** + * 根据父类过滤模型类(支持多层继承) + * @param {string | ModelConstructor} parentClass 父类名称或构造函数 + * @returns {Map} 继承自指定父类的模型类映射 + */ + public filterModelClassByParent(parentClass: string | ModelConstructor) { + const parentModelClass = typeof parentClass === 'string' ? this.getModelClass(parentClass) : parentClass; + if (!parentModelClass) { + return new Map(); + } + const modelClasses = new Map(); + for (const [className, ModelClass] of this.modelClasses) { + if (isInheritedFrom(ModelClass, parentModelClass)) { + modelClasses.set(className, ModelClass); + } + } + return modelClasses; + } +} diff --git a/packages/core/flow-engine/src/flowSettings.ts b/packages/core/flow-engine/src/flowSettings.ts new file mode 100644 index 0000000000..d079599eb5 --- /dev/null +++ b/packages/core/flow-engine/src/flowSettings.ts @@ -0,0 +1,183 @@ +/** + * 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 { openStepSettingsDialog } from './components/settings/wrappers/contextual/StepSettingsDialog'; +import { StepSettingsDialogProps } from './types'; + +export class FlowSettings { + public components: Record = {}; + public scopes: Record = {}; + private antdComponentsLoaded = false; + + /** + * 加载 FlowSettings 所需的资源。 + * @returns {Promise} + * @example + * await flowSettings.load(); + */ + public async load(): Promise { + if (this.antdComponentsLoaded) { + console.log('FlowSettings: Antd components already loaded, skipping...'); + return; + } + + try { + // 动态导入 Antd 组件 + const { + ArrayBase, + ArrayCards, + ArrayCollapse, + ArrayItems, + ArrayTable, + ArrayTabs, + Cascader, + Checkbox, + DatePicker, + Editable, + Form, + FormDialog, + FormDrawer, + FormButtonGroup, + FormCollapse, + FormGrid, + FormItem, + FormLayout, + FormStep, + FormTab, + Input, + NumberPicker, + Password, + PreviewText, + Radio, + Reset, + Select, + SelectTable, + Space, + Submit, + Switch, + TimePicker, + Transfer, + TreeSelect, + Upload, + } = await import('@formily/antd-v5'); + + // 单独导入 Button 组件 + const { Button } = await import('antd'); + + // 注册基础组件 + this.components.Form = Form; + this.components.FormDialog = FormDialog; + this.components.FormDrawer = FormDrawer; + this.components.FormItem = FormItem; + this.components.FormLayout = FormLayout; + this.components.FormGrid = FormGrid; + this.components.FormStep = FormStep; + this.components.FormTab = FormTab; + this.components.FormCollapse = FormCollapse; + this.components.FormButtonGroup = FormButtonGroup; + + // 注册输入组件 + this.components.Input = Input; + this.components.NumberPicker = NumberPicker; + this.components.Password = Password; + + // 注册选择组件 + this.components.Select = Select; + this.components.SelectTable = SelectTable; + this.components.Cascader = Cascader; + this.components.TreeSelect = TreeSelect; + this.components.Transfer = Transfer; + + // 注册日期时间组件 + this.components.DatePicker = DatePicker; + this.components.TimePicker = TimePicker; + + // 注册选择组件 + this.components.Checkbox = Checkbox; + this.components.Radio = Radio; + this.components.Switch = Switch; + + // 注册数组组件 + this.components.ArrayBase = ArrayBase; + this.components.ArrayCards = ArrayCards; + this.components.ArrayCollapse = ArrayCollapse; + this.components.ArrayItems = ArrayItems; + this.components.ArrayTable = ArrayTable; + this.components.ArrayTabs = ArrayTabs; + + // 注册其他组件 + this.components.Upload = Upload; + this.components.Space = Space; + this.components.Editable = Editable; + this.components.PreviewText = PreviewText; + + // 注册按钮组件 + this.components.Button = Button; + this.components.Submit = Submit; + this.components.Reset = Reset; + + this.antdComponentsLoaded = true; + console.log('FlowSettings: Antd components loaded successfully'); + } catch (error) { + console.error('FlowSettings: Failed to load Antd components:', error); + throw error; + } + } + + /** + * 添加组件到 FlowSettings 的组件注册表中。 + * 这些组件可以在 flow step 的 uiSchema 中使用。 + * @param {Record} components 要添加的组件对象 + * @returns {void} + * @example + * flowSettings.registerComponents({ MyComponent, AnotherComponent }); + */ + public registerComponents(components: Record): void { + Object.keys(components).forEach((name) => { + if (this.components[name]) { + console.warn(`FlowSettings: Component with name '${name}' is already registered and will be overwritten.`); + } + this.components[name] = components[name]; + }); + } + + /** + * 添加作用域到 FlowSettings 的作用域注册表中。 + * 这些作用域可以在 flow step 的 uiSchema 中使用。 + * @param {Record} scopes 要添加的作用域对象 + * @returns {void} + * @example + * flowSettings.registerScopes({ useMyHook, myVariable, myFunction }); + */ + public registerScopes(scopes: Record): void { + Object.keys(scopes).forEach((name) => { + if (this.scopes[name]) { + console.warn(`FlowSettings: Scope with name '${name}' is already registered and will be overwritten.`); + } + this.scopes[name] = scopes[name]; + }); + } + + /** + * 显示单个步骤的配置界面 + * @param {StepSettingsDialogProps} props 步骤设置对话框的属性 + * @returns {Promise} 返回表单提交的值 + * @example + * const result = await flowSettings.openStepSettingsDialog({ + * model: myModel, + * flowKey: 'myFlow', + * stepKey: 'myStep', + * dialogWidth: 800, + * dialogTitle: '自定义标题' + * }); + */ + public async openStepSettingsDialog(props: StepSettingsDialogProps): Promise { + return await openStepSettingsDialog(props); + } +} diff --git a/packages/core/flow-engine/src/hooks/index.ts b/packages/core/flow-engine/src/hooks/index.ts new file mode 100644 index 0000000000..cd6772c030 --- /dev/null +++ b/packages/core/flow-engine/src/hooks/index.ts @@ -0,0 +1,13 @@ +/** + * 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 './useFlowExtraContext'; +export * from './useFlowModel'; +export * from './useApplyFlow'; +export * from './useDispatchEvent'; diff --git a/packages/core/flow-engine/src/hooks/useApplyFlow.ts b/packages/core/flow-engine/src/hooks/useApplyFlow.ts new file mode 100644 index 0000000000..cbe4b481cc --- /dev/null +++ b/packages/core/flow-engine/src/hooks/useApplyFlow.ts @@ -0,0 +1,239 @@ +/** + * 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 { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { autorun, toJS } from '@formily/reactive'; +import { FlowModel } from '../models'; +import { useFlowEngine } from '../provider'; +import { FlowExtraContext } from '../types'; +import { uid } from 'uid/secure'; + +// 生成稳定的缓存键 +function generateCacheKey(prefix: string, flowKey: string, modelUid: string): string { + return `${prefix}:${flowKey}:${modelUid}`; +} + +// 安全序列化对象的函数 +function safeStringify(obj: any, visited = new Set(), depth = 0, maxDepth = 5): string { + // 深度限制,防止过深递归 + if (depth > maxDepth) { + return '[MaxDepthExceeded]'; + } + + // 处理基本类型 + if (obj === null || obj === undefined) { + return String(obj); + } + + if (typeof obj !== 'object' && typeof obj !== 'function') { + return String(obj); + } + + // 处理函数 + if (typeof obj === 'function') { + // 对于函数,可以使用函数名或为匿名函数生成一个标识符 + return `function:${obj.name || 'anonymous'}`; + } + + // 处理循环引用 + if (visited.has(obj)) { + return '[Circular]'; + } + + // 添加到已访问集合 + visited.add(obj); + + // 处理数组 + if (Array.isArray(obj)) { + try { + // 限制处理的数组元素数量 + const maxItems = 100; + const items = obj.slice(0, maxItems); + const result = + '[' + + items.map((item) => safeStringify(item, visited, depth + 1, maxDepth)).join(',') + + (obj.length > maxItems ? ',...' : '') + + ']'; + return result; + } catch (e) { + return '[Array]'; + } + } + + // 处理普通对象 + try { + const keys = Object.keys(obj).sort(); // 排序确保稳定性 + // 限制处理的属性数量 + const maxKeys = 50; + const limitedKeys = keys.slice(0, maxKeys); + + const pairs = limitedKeys.map((key) => { + const value = obj[key]; + return `${key}:${safeStringify(value, visited, depth + 1, maxDepth)}`; + }); + + return '{' + pairs.join(',') + (keys.length > maxKeys ? ',...' : '') + '}'; + } catch (e) { + // 如果无法序列化,返回一个简单的标识 + return '[Object]'; + } +} + +/** + * 通用的流程执行 Hook + * @param cacheKeyPrefix 缓存键前缀 + * @param flowKey 流程键 + * @param model FlowModel 实例 + * @param context 用户上下文 + * @param executor 执行函数 + * @param logMessage 日志消息 + * @returns 执行结果 + */ +function useFlowExecutor( + cacheKeyPrefix: string, + flowKey: string, + model: TModel, + context: FlowExtraContext | undefined, + executor: (context?: FlowExtraContext) => Promise, + logMessage: string, +): T { + const engine = useFlowEngine(); + const cacheKey = useMemo( + () => generateCacheKey(cacheKeyPrefix, flowKey, model.uid), + [cacheKeyPrefix, flowKey, model.uid], + ); + const [, forceUpdate] = useState({}); + const isMounted = useRef(false); + const flowEngineCache = engine.applyFlowCache; + + useEffect(() => { + isMounted.current = true; + return () => { + flowEngineCache.delete(cacheKey); + }; + }, [cacheKey]); + + useEffect(() => { + let debounceTimer: NodeJS.Timeout | null = null; + let isInitialAutorunForEffect = true; + + const disposeAutorun = autorun(() => { + // 只监听 stepParams 的变化,移除对 props 的监听以避免循环触发 + JSON.stringify(toJS(model.stepParams)); + + if (isInitialAutorunForEffect) { + isInitialAutorunForEffect = false; + if (flowEngineCache.get(cacheKey)) { + return; + } + } + + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(async () => { + if (!isMounted.current) return; + + const cachedEntry = flowEngineCache.get(cacheKey); + if (cachedEntry?.status === 'resolved' || !cachedEntry) { + console.log(logMessage); + try { + const newResult = await executor(context); + if (isMounted.current) { + flowEngineCache.set(cacheKey, { + status: 'resolved', + data: newResult, + promise: Promise.resolve(newResult), + }); + forceUpdate({}); + } + } catch (newError) { + flowEngineCache.set(cacheKey, { status: 'rejected', error: newError, promise: Promise.reject(newError) }); + forceUpdate({}); + } + } + }, 300); + }); + + return () => { + if (debounceTimer) clearTimeout(debounceTimer); + disposeAutorun(); + }; + }, [model, context, cacheKey, executor, logMessage, engine]); + + // 检查缓存 + const cachedEntry = flowEngineCache.get(cacheKey); + if (cachedEntry) { + if (cachedEntry.status === 'resolved') return cachedEntry.data; + if (cachedEntry.status === 'rejected') throw cachedEntry.error; + if (cachedEntry.status === 'pending') throw cachedEntry.promise; + } + + // 执行初始请求 + const promise = executor(context) + .then((result) => { + flowEngineCache.set(cacheKey, { status: 'resolved', data: result, promise }); + return result; + }) + .catch((err) => { + flowEngineCache.set(cacheKey, { status: 'rejected', error: err, promise }); + throw err; + }); + + flowEngineCache.set(cacheKey, { status: 'pending', promise }); + throw promise; +} + +export function useApplyFlow( + flowKey: string, + model: TModel, + context?: FlowExtraContext, +): any { + return useFlowExecutor( + 'applyFlow', + flowKey, + model, + context, + (ctx) => model.applyFlow(flowKey, ctx), + `[FlowEngine.useApplyFlow] Reactive re-apply for flow: ${flowKey}, model: ${model.uid}`, + ); +} + +/** + * Hook for applying auto-apply flows on a FlowModel + * @param model The FlowModel instance + * @param context Optional user context + * @returns The results of all auto-apply flows execution + */ +export function useApplyAutoFlows(modelOrUid: FlowModel | string, context?: FlowExtraContext, independentAutoFlowExecution?: boolean): any[] { + const flowEngine = useFlowEngine(); + const model = useMemo(() => { + if (typeof modelOrUid === 'string') { + return flowEngine.getModel(modelOrUid); + } + return modelOrUid; + }, [modelOrUid, flowEngine]); + + // const runId = useMemo(() => { + // if (independentAutoFlowExecution) { + // return `autoFlow:${uid()}`; + // } + // return 'autoFlow'; + // }, [independentAutoFlowExecution]); + + const executor = useCallback((ctx?: FlowExtraContext) => model.applyAutoFlows(ctx), [model]); + + return useFlowExecutor( + // runId, + 'autoFlow', + 'all', + model, + context, + executor, + `[FlowEngine.useApplyAutoFlows] Reactive re-apply for model: ${model.uid}`, + ); +} diff --git a/packages/core/flow-engine/src/hooks/useDispatchEvent.ts b/packages/core/flow-engine/src/hooks/useDispatchEvent.ts new file mode 100644 index 0000000000..0379e52cfb --- /dev/null +++ b/packages/core/flow-engine/src/hooks/useDispatchEvent.ts @@ -0,0 +1,20 @@ +/** + * 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 { useCallback } from 'react'; +import { FlowModel } from '../models'; +import { FlowExtraContext } from '../types'; + +export function useDispatchEvent(eventName: string, model: FlowModel, context?: FlowExtraContext) { + const dispatch = useCallback(() => { + model.dispatchEvent(eventName, context); + }, [model, eventName, context]); + + return { dispatch }; +} diff --git a/packages/core/flow-engine/src/hooks/useFlowExtraContext.ts b/packages/core/flow-engine/src/hooks/useFlowExtraContext.ts new file mode 100644 index 0000000000..fbb8991b77 --- /dev/null +++ b/packages/core/flow-engine/src/hooks/useFlowExtraContext.ts @@ -0,0 +1,34 @@ +/** + * 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 { useMemo } from 'react'; +import { useFlowEngine } from '../provider'; +// import { useApp } from '@nocobase/client'; // 移除对 @nocobase/client 的依赖 + +/** + * Hook to get extra context for flow execution + * This is used to prepare context data before calling applyFlow + * @returns Extra context object that can be passed to applyFlow + */ +export function useFlowExtraContext(): Record { + // const app = useApp(); // 移除 app 的获取 + + // 创建并返回额外上下文对象 + const extraContext = useMemo( + () => ({ + // // 可以在这里添加从 React 上下文中获取的数据 + // globals: engine.getContext() || {}, + // timestamp: new Date().toISOString(), + // // app: app, // 如果需要的话可以添加 app 实例 + }), + [], + ); + + return extraContext; +} diff --git a/packages/core/flow-engine/src/hooks/useFlowModel.ts b/packages/core/flow-engine/src/hooks/useFlowModel.ts new file mode 100644 index 0000000000..8287c3e711 --- /dev/null +++ b/packages/core/flow-engine/src/hooks/useFlowModel.ts @@ -0,0 +1,38 @@ +/** + * 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 { useMemo } from 'react'; +import { FlowModel } from '../models'; +// import { useApp } from '@nocobase/client'; // 移除 useApp +import { useFlowEngine } from '../provider'; +import { StepParams } from '../types'; + +export function useFlowModel( + uid: string, + modelClassName?: string, + stepParams?: StepParams, +): T { + const engine = useFlowEngine(); + // const app = useApp(); // 移除 app + + const model = useMemo(() => { + let instance = engine.getModel(uid); + if (!instance && modelClassName) { + instance = engine.createModel({ + uid, + use: modelClassName, + stepParams, + // app: app, // createModel 的 app 参数已被移除 + }); + } + return instance; + }, [engine, /* app, */ modelClassName, uid, stepParams]); // 移除 app 从依赖数组 + + return model; +} diff --git a/packages/core/flow-engine/src/index.ts b/packages/core/flow-engine/src/index.ts new file mode 100644 index 0000000000..f72813ae5d --- /dev/null +++ b/packages/core/flow-engine/src/index.ts @@ -0,0 +1,28 @@ +/** + * 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 './types'; + +// 工具函数 +export * from './utils'; + +// 资源类 +export * from './resources'; + +// 区块模型类, 理论上各个区块自己提供 +export * from './flowEngine'; +export * from './hooks'; +export * from './models'; +export * from './provider'; +export * from './withFlowModel'; + +export * from './components'; +export * from './data-source'; +// diff --git a/packages/core/flow-engine/src/models/actions/actionModel.ts b/packages/core/flow-engine/src/models/actions/actionModel.ts new file mode 100644 index 0000000000..3323eab70d --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/actionModel.ts @@ -0,0 +1,103 @@ +/** + * 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 { BlockModel } from '..'; +import { StepParams } from '../../types'; +import { FlowModel } from '../flowModel'; + +export class ActionModel extends FlowModel { + public blockModel: BlockModel; + + static { + this.initFlows(); + } + + /** + * 设置BlockModel实例 + * @param {BlockModel} blockModel BlockModel实例 + */ + setBlockModel(blockModel: BlockModel): void { + this.blockModel = blockModel; + } + + protected static initFlows() { + this.registerFlow({ + key: 'default', + auto: true, + title: '按钮属性', + steps: { + setText: { + title: '文本', + handler: (ctx, params) => ctx.model.setProps('text', params.text), + uiSchema: { + text: { + type: 'string', + title: '标题', + 'x-component': 'Input', + }, + }, + defaultParams: { text: '操作' }, + }, + setDanger: { + title: '是否danger', + handler: (ctx, params) => ctx.model.setProps('danger', params.danger), + uiSchema: { + danger: { + type: 'boolean', + title: '是否危险', + 'x-component': 'Switch', + }, + }, + defaultParams: { danger: false }, + }, + setType: { + title: '按钮类型', + handler: (ctx, params) => ctx.model.setProps('type', params.type), + uiSchema: { + type: { + type: 'string', + title: '类型', + 'x-component': 'Select', + enum: [ + { label: 'Primary', value: 'primary' }, + { label: 'Default', value: 'default' }, + ], + }, + }, + defaultParams: { type: 'default' }, + }, + setOnClick: { + title: '点击事件', + handler: (ctx, params) => { + ctx.model.setProps('onClick', () => { + ctx.model.dispatchEvent('onClick', ctx); + }); + }, + }, + }, + }); + + this.registerFlow({ + key: 'remove', + on: { + eventName: 'remove', + }, + steps: { + remove: { + title: '移除操作', + handler: async (ctx, params) => { + ctx.model.blockModel.removeAction(ctx.model.uid); + await ctx.model.flowEngine.destroyModel(ctx.model.blockModel.uid); + await ctx.model.blockModel.applyAutoFlows(); + }, + }, + }, + }); + } +} diff --git a/packages/core/flow-engine/src/models/actions/deleteActionModel.ts b/packages/core/flow-engine/src/models/actions/deleteActionModel.ts new file mode 100644 index 0000000000..7a19786868 --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/deleteActionModel.ts @@ -0,0 +1,63 @@ +/** + * 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 { ActionModel } from './actionModel'; +import { Modal } from 'antd'; + +export const DeleteActionModel = ActionModel.extends([ + { + key: 'onClick', + title: '删除操作', + steps: { + showConfirm: { + title: '确认弹窗', + handler: async (ctx, params) => { + if (params.showConfirm) { + await new Promise((resolve) => { + Modal.confirm({ + title: params.title, + content: params.content, + onOk: () => resolve(true), + onCancel: () => { + ctx.exit(); + resolve(false); + }, + }); + }); + } + }, + defaultParams: { + showConfirm: true, + title: '确定删除吗?', + content: '删除后无法恢复,请谨慎操作。', + }, + }, + deleteData: { + handler: (ctx, params) => { + console.log('delete', ctx, ctx.model, params); + Modal.success({ + title: '删除成功', + content: '数据删除成功。', + }); + }, + }, + }, + }, + { + key: 'default', + patch: true, + steps: { + setText: { + defaultParams: { + text: '删除', + }, + }, + }, + }, +]); diff --git a/packages/core/flow-engine/src/models/actions/index.ts b/packages/core/flow-engine/src/models/actions/index.ts new file mode 100644 index 0000000000..4772a32749 --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/index.ts @@ -0,0 +1,13 @@ +/** + * 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 './actionModel'; +export * from './deleteActionModel'; +export * from './saveActionModel'; +export * from './refreshActionModel'; diff --git a/packages/core/flow-engine/src/models/actions/refreshActionModel.ts b/packages/core/flow-engine/src/models/actions/refreshActionModel.ts new file mode 100644 index 0000000000..ea7ea92df8 --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/refreshActionModel.ts @@ -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 { Modal } from 'antd'; +import { ActionModel } from './actionModel'; + +export const RefreshActionModel = ActionModel.extends([ + { + key: 'onClick', + title: '刷新操作', + steps: { + refresh: { + handler: (ctx, params) => { + console.log('refresh', ctx, ctx.model, params); + Modal.success({ + title: '刷新成功', + content: '数据刷新成功。', + }); + }, + }, + }, + }, + { + key: 'default', + patch: true, + steps: { + setText: { + defaultParams: { + text: '刷新', + }, + }, + }, + }, +]); diff --git a/packages/core/flow-engine/src/models/actions/saveActionModel.ts b/packages/core/flow-engine/src/models/actions/saveActionModel.ts new file mode 100644 index 0000000000..5cb3cabb6d --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/saveActionModel.ts @@ -0,0 +1,39 @@ +/** + * 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 { ActionModel } from './actionModel'; +import { Modal } from 'antd'; +export const SaveActionModel = ActionModel.extends([ + { + key: 'onClick', + title: '保存操作', + steps: { + save: { + handler: (ctx, params) => { + console.log('save', ctx, ctx.model, params); + Modal.success({ + title: '保存成功', + content: '数据保存成功。', + }); + }, + }, + }, + }, + { + key: 'default', + patch: true, + steps: { + setText: { + defaultParams: { + text: '保存', + }, + }, + }, + }, +]); diff --git a/packages/core/flow-engine/src/models/actions/updateActionModel.ts b/packages/core/flow-engine/src/models/actions/updateActionModel.ts new file mode 100644 index 0000000000..5d1dbace23 --- /dev/null +++ b/packages/core/flow-engine/src/models/actions/updateActionModel.ts @@ -0,0 +1,35 @@ +/** + * 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 { ActionModel } from './actionModel'; + +export const UpdateActionModel = ActionModel.extends([ + { + key: 'onClick', + title: '更新操作', + steps: { + update: { + handler: (ctx, params) => { + console.log('update', ctx, ctx.model, params); + }, + }, + }, + }, + { + key: 'default', + patch: true, + steps: { + setText: { + defaultParams: { + text: '更新', + }, + }, + }, + }, +]); diff --git a/packages/core/flow-engine/src/models/blockModel.ts b/packages/core/flow-engine/src/models/blockModel.ts new file mode 100644 index 0000000000..9843705463 --- /dev/null +++ b/packages/core/flow-engine/src/models/blockModel.ts @@ -0,0 +1,180 @@ +/** + * 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 './flowModel'; +import { ActionModel } from './actions/actionModel'; +import { define, observable } from '@formily/reactive'; +import { DeleteActionModel } from './actions/deleteActionModel'; +import { RefreshActionModel } from './actions/refreshActionModel'; +import { ExtendedFlowDefinition, StepParams } from '../types'; +import { UpdateActionModel } from './actions/updateActionModel'; + +const registeredActionModels = new WeakMap< + typeof BlockModel, + Set<{ + title: string; + type: typeof ActionModel; + }> +>(); + +export class BlockModel extends FlowModel { + public actions: Map; + + static { + this.initFlows(); + this.initSupportedActions(); + } + + constructor(options: { uid: string; stepParams?: StepParams }) { + super({ + uid: options.uid, + stepParams: options.stepParams, + }); + this.actions = observable(new Map()); + define(this, { + actions: observable, + }); + } + + setActions(actions: ActionModel[]) { + this.actions.clear(); + actions.forEach((action) => this.actions.set(action.uid, action)); + } + + addAction(action: ActionModel) { + this.actions.set(action.uid, action); + } + + getAction(uid: string): ActionModel | undefined { + return this.actions.get(uid); + } + + removeAction(uid: string): boolean { + return this.actions.delete(uid); + } + + getActions(): ActionModel[] { + return Array.from(this.actions.values()); + } + + public static registerActionModel({ title, type }: { title: string; type: typeof ActionModel }) { + registeredActionModels.get(this) || registeredActionModels.set(this, new Set()); + registeredActionModels.get(this)?.add({ title, type }); + } + + public static get supportedActions(): { + title: string; + type: typeof ActionModel; + }[] { + // 收集当前类和所有父类的 actions(继承链向上查找) + const allActions = new Set<{ + title: string; + type: typeof ActionModel; + }>(); + + let currentClass = this; + // 向上遍历继承链,直到到达 BlockModel 的基类 + while (currentClass && typeof currentClass === 'function') { + const actions = registeredActionModels.get(currentClass); + if (actions) { + actions.forEach((action) => allActions.add(action)); + } + + // 向上查找父类 + const parentClass = Object.getPrototypeOf(currentClass); + // 如果父类不再是 BlockModel 的子类,则停止(避免越过 BlockModel 基类边界) + if (parentClass === FlowModel || parentClass === Function.prototype) { + break; + } + currentClass = parentClass; + } + + return Array.from(allActions); + } + + protected static initFlows() { + this.registerFlow({ + key: 'default', + auto: true, + title: '区块通用属性', + steps: { + setHeight: { + title: '设置高度', + handler: (ctx, params) => { + const { height } = params || {}; + if (height !== undefined) { + ctx.model.setProps('height', height); + } + }, + uiSchema: { + height: { + type: 'number', + 'x-component': 'InputNumber', + }, + }, + defaultParams: { height: 300 }, + }, + }, + }); + } + + protected static initSupportedActions() { + this.registerActionModel({ + title: '删除', + type: DeleteActionModel, + }); + // this.registerActionModel({ + // title: '保存', + // type: SaveActionModel, + // }); + this.registerActionModel({ + title: '刷新', + type: RefreshActionModel, + }); + this.registerActionModel({ + title: '更新', + type: UpdateActionModel, + }); + } + + /** + * 覆盖父类的 extends 方法,支持 supportedActions 参数 + */ + public static extends(this: T, flows: ExtendedFlowDefinition[]): T; + + public static extends( + this: T, + flows: ExtendedFlowDefinition[], + supportedActions: { + title: string; + type: typeof ActionModel; + }[], + ): T; + + public static extends( + this: T, + flows: ExtendedFlowDefinition[] = [], + supportedActions?: { + title: string; + type: typeof ActionModel; + }[], + ): T { + // 先调用父类的 extends 方法处理 flows + const ExtendedClass = FlowModel.extends.call(this, flows) as T; + + // 注册 supportedActions + if (supportedActions && supportedActions.length > 0) { + supportedActions.forEach((action) => { + ExtendedClass.registerActionModel(action); + }); + } + + return ExtendedClass; + } +} diff --git a/packages/core/flow-engine/src/models/dataBlockModel.ts b/packages/core/flow-engine/src/models/dataBlockModel.ts new file mode 100644 index 0000000000..0ad9305809 --- /dev/null +++ b/packages/core/flow-engine/src/models/dataBlockModel.ts @@ -0,0 +1,105 @@ +/** + * 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 { BlockModel } from './blockModel'; +import { FlowResource } from '../resources/flowResource'; + +export class DataBlockModel extends BlockModel { + public resource: FlowResource; + public fields: any[]; + + static { + this.initFlows(); + } + + constructor(options: { uid: string; stepParams?: Record; resource?: FlowResource }) { + super({ + uid: options.uid, + stepParams: options.stepParams, + }); + if (options.resource) { + this.resource = options.resource; + } + this.fields = []; + } + + /** + * 设置Resource实例 + * @param {FlowResource} resource FlowResource实例 + */ + setResource(resource: FlowResource): void { + this.resource = resource; + } + + setFields(fields: any[]) { + this.fields = fields; + } + + getFields() { + return this.fields; + } + + protected static initFlows() { + // 添加 DataBlockModel 特有的 flows + // 保存(修改) data + // 添加 data + // 删除 data + this.registerFlow({ + key: 'saveData', + on: { + eventName: 'saveData', + }, + steps: { + save: { + handler: (ctx, params) => { + console.log('saveData', ctx, ctx.model, params); + }, + }, + }, + }); + + this.registerFlow({ + key: 'addData', + on: { + eventName: 'addData', + }, + steps: { + add: { + handler: (ctx, params) => { + console.log('addData', ctx, ctx.model, params); + }, + }, + }, + }); + + this.registerFlow({ + key: 'deleteData', + on: { + eventName: 'deleteData', + }, + steps: { + delete: { + handler: (ctx, params) => { + console.log('deleteData', ctx, ctx.model, params); + }, + uiSchema: { + showConfirm: { + type: 'boolean', + title: '是否二次确认', + 'x-component': 'Switch', + }, + }, + defaultParams: { + showConfirm: true, + }, + }, + }, + }); + } +} diff --git a/packages/core/flow-engine/src/models/flowModel.tsx b/packages/core/flow-engine/src/models/flowModel.tsx new file mode 100644 index 0000000000..bac0eb7998 --- /dev/null +++ b/packages/core/flow-engine/src/models/flowModel.tsx @@ -0,0 +1,678 @@ +/** + * 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 { action, define, observable } from '@formily/reactive'; +import _ from 'lodash'; +import React from 'react'; +import { uid } from 'uid/secure'; +import { openRequiredParamsStepFormDialog as openRequiredParamsStepFormDialogFn } from '../components/settings/wrappers/contextual/StepRequiredSettingsDialog'; +import { openStepSettingsDialog as openStepSettingsDialogFn } from '../components/settings/wrappers/contextual/StepSettingsDialog'; +import { FlowEngine } from '../flowEngine'; +import type { + ActionStepDefinition, + ArrayElementType, + CreateModelOptions, + CreateSubModelOptions, + DefaultStructure, + FlowContext, + FlowDefinition, + FlowModelMeta, + FlowModelOptions, + InlineStepDefinition, + StepDefinition, + StepParams, +} from '../types'; +import { ExtendedFlowDefinition, FlowExtraContext, IModelComponentProps, ReadonlyModelProps } from '../types'; +import { generateUid, mergeFlowDefinitions } from '../utils'; + +// 使用WeakMap存储每个类的meta +const modelMetas = new WeakMap(); + +// 使用WeakMap存储每个类的flows +const modelFlows = new WeakMap>(); + +export class FlowModel { + public readonly uid: string; + public props: IModelComponentProps = {}; + public stepParams: StepParams = {}; + public flowEngine: FlowEngine; + public parent: Structure['parent']; + public subModels: Structure['subModels']; + // public static meta: FlowModelMeta; + + constructor(protected options: FlowModelOptions) { + if (options?.flowEngine?.getModel(options.uid)) { + // 此时 new FlowModel 并不创建新实例,而是返回已存在的实例,避免重复创建同一个model实例 + return options.flowEngine.getModel(options.uid); + } + + this.uid = options.uid || uid(); + this.props = options.props || {}; + this.stepParams = options.stepParams || {}; + this.subModels = {}; + this.flowEngine = options.flowEngine; + + define(this, { + props: observable, + subModels: observable, + stepParams: observable, + setProps: action, + setStepParams: action, + }); + // 保证onInit在所有属性都定义完成后调用 + // queueMicrotask(() => { + // this.onInit(options); + // }); + this.createSubModels(options.subModels); + } + + onInit(options): void {} + + static get meta() { + return modelMetas.get(this); + } + + private createSubModels(subModels: Record) { + Object.entries(subModels || {}).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => { + this.addSubModel(key, item); + }); + } else { + this.setSubModel(key, value); + } + }); + } + + /** + * 设置FlowEngine实例 + * @param {FlowEngine} flowEngine FlowEngine实例 + */ + setFlowEngine(flowEngine: FlowEngine): void { + this.flowEngine = flowEngine; + } + + static define(meta: FlowModelMeta) { + modelMetas.set(this, meta); + } + + /** + * 注册一个流程 (Flow)。支持泛型,能够正确推导出模型类型。 + * @template TModel 具体的FlowModel子类类型 + * @param {string | FlowDefinition} keyOrDefinition 流程的 Key 或 FlowDefinition 对象。 + * 如果为字符串,则为流程 Key,需要配合 flowDefinition 参数。 + * 如果为对象,则为包含 key 属性的完整 FlowDefinition。 + * @param {FlowDefinition} [flowDefinition] 当第一个参数为流程 Key 时,此参数为流程的定义。 + * @returns {void} + */ + public static registerFlow FlowModel>( + this: TModel, + keyOrDefinition: string | FlowDefinition>, + flowDefinition?: Omit>, 'key'> & { key?: string }, + ): void { + let definition: FlowDefinition>; + let key: string; + + if (typeof keyOrDefinition === 'string' && flowDefinition) { + key = keyOrDefinition; + definition = { + ...flowDefinition, + key, + }; + } else if (typeof keyOrDefinition === 'object' && 'key' in keyOrDefinition) { + key = keyOrDefinition.key; + definition = keyOrDefinition; + } else { + throw new Error('Invalid arguments for registerFlow'); + } + + // 确保当前类有自己的flows Map + const ModelClass = this; + if (!modelFlows.has(ModelClass as any)) { + modelFlows.set(ModelClass as any, new Map()); + } + const flows = modelFlows.get(ModelClass as any)!; + + if (flows.has(key)) { + console.warn(`FlowModel: Flow with key '${key}' is already registered and will be overwritten.`); + } + flows.set(key, definition as FlowDefinition); + } + + /** + * 扩展已存在的流程定义。通过合并现有流程和扩展定义来创建新的流程。 + * @template TModel 具体的FlowModel子类类型 + * @param {string | ExtendedFlowDefinition} keyOrDefinition 流程的 Key 或 ExtendedFlowDefinition 对象。 + * 如果为字符串,则为流程 Key,需要配合 extendDefinition 参数。 + * 如果为对象,则为包含 key 属性的完整 ExtendedFlowDefinition。 + * @param {Omit} [extendDefinition] 当第一个参数为流程 Key 时,此参数为流程的扩展定义。 + * @returns {void} + */ + public static extendFlow( + keyOrDefinition: string | ExtendedFlowDefinition, + extendDefinition?: Omit, + ): void { + let definition: ExtendedFlowDefinition; + let key: string; + + if (typeof keyOrDefinition === 'string' && extendDefinition) { + key = keyOrDefinition; + definition = { + ...extendDefinition, + key, + }; + } else if (typeof keyOrDefinition === 'object' && 'key' in keyOrDefinition) { + key = keyOrDefinition.key; + definition = keyOrDefinition; + } else { + throw new Error('Invalid arguments for extendFlow'); + } + + // 获取所有流程(包括从父类继承的) + const allFlows = this.getFlows(); + const originalFlow = allFlows.get(key); + + if (!originalFlow) { + console.warn( + `FlowModel.extendFlow: Cannot extend flow '${key}' as it does not exist in parent class. Registering as new flow.`, + ); + // 移除patch标记,作为新流程注册 + const { patch, ...newFlowDef } = definition; + this.registerFlow(newFlowDef as FlowDefinition); + return; + } + + // 合并流程定义 + const mergedFlow = mergeFlowDefinitions(originalFlow, definition); + + // 注册合并后的流程 + this.registerFlow(mergedFlow as FlowDefinition); + } + + /** + * 获取已注册的流程定义。 + * 如果当前类不存在对应的flow,会继续往父类查找。 + * @param {string} key 流程 Key。 + * @returns {FlowDefinition | undefined} 流程定义,如果未找到则返回 undefined。 + */ + public getFlow(key: string): FlowDefinition | undefined { + // 获取当前类的构造函数 + const currentClass = this.constructor as typeof FlowModel; + + // 遍历类继承链,查找流程 + let cls: typeof FlowModel | null = currentClass; + while (cls) { + const flows = modelFlows.get(cls); + if (flows && flows.has(key)) { + return flows.get(key); + } + + // 获取父类 + const proto = Object.getPrototypeOf(cls); + if (proto === Function.prototype || proto === Object.prototype) { + break; + } + cls = proto as typeof FlowModel; + } + + return undefined; + } + + /** + * 获取所有已注册的流程定义,包括从父类继承的流程。 + * @returns {Map} 一个包含所有流程定义的 Map 对象,Key 为流程 Key,Value 为流程定义。 + */ + public static getFlows(): Map { + // 创建一个新的Map来存储所有流程 + const allFlows = new Map(); + + // 遍历类继承链,收集所有流程 + let cls: typeof FlowModel | null = this; + while (cls) { + const flows = modelFlows.get(cls); + if (flows) { + // 合并流程,但如果已有同名流程则不覆盖 + for (const [key, flow] of flows.entries()) { + if (!allFlows.has(key)) { + allFlows.set(key, flow); + } + } + } + + // 获取父类 + const proto = Object.getPrototypeOf(cls); + if (proto === Function.prototype || proto === Object.prototype) { + break; + } + cls = proto as typeof FlowModel; + } + + return allFlows; + } + + setProps(props: IModelComponentProps): void; + setProps(key: string, value: any): void; + setProps(props: IModelComponentProps | string, value?: any): void { + if (typeof props === 'string') { + this.props[props] = value; + } else { + this.props = { ...props }; + } + } + + getProps(): ReadonlyModelProps { + return this.props as ReadonlyModelProps; + } + + setStepParams(flowKey: string, stepKey: string, params: any): void; + setStepParams(flowKey: string, stepParams: Record): void; + setStepParams(allParams: StepParams): void; + setStepParams( + flowKeyOrAllParams: string | StepParams, + stepKeyOrStepsParams?: string | Record, + params?: any, + ): void { + if (typeof flowKeyOrAllParams === 'string') { + const flowKey = flowKeyOrAllParams; + if (typeof stepKeyOrStepsParams === 'string' && params !== undefined) { + if (!this.stepParams[flowKey]) { + this.stepParams[flowKey] = {}; + } + this.stepParams[flowKey][stepKeyOrStepsParams] = params; + } else if (typeof stepKeyOrStepsParams === 'object' && stepKeyOrStepsParams !== null) { + this.stepParams[flowKey] = { ...(this.stepParams[flowKey] || {}), ...stepKeyOrStepsParams }; + } + } else if (typeof flowKeyOrAllParams === 'object' && flowKeyOrAllParams !== null) { + for (const fk in flowKeyOrAllParams) { + if (Object.prototype.hasOwnProperty.call(flowKeyOrAllParams, fk)) { + this.stepParams[fk] = { ...(this.stepParams[fk] || {}), ...flowKeyOrAllParams[fk] }; + } + } + } + } + + getStepParams(flowKey: string, stepKey: string): any | undefined; + getStepParams(flowKey: string): Record | undefined; + getStepParams(): StepParams; + getStepParams(flowKey?: string, stepKey?: string): any { + if (flowKey && stepKey) { + return this.stepParams[flowKey]?.[stepKey]; + } + if (flowKey) { + return this.stepParams[flowKey]; + } + return this.stepParams; + } + + async applyFlow(flowKey: string, extra?: FlowExtraContext): Promise { + const currentFlowEngine = this.flowEngine; + if (!currentFlowEngine) { + console.warn( + 'FlowEngine not available on this model for applyFlow. Check model.app and model.app.flowEngine setup.', + ); + return Promise.reject(new Error('FlowEngine not available for applyFlow. Please set flowEngine on the model.')); + } + + const flow = this.getFlow(flowKey); + + if (!flow) { + console.error(`BaseModel.applyFlow: Flow with key '${flowKey}' not found.`); + return Promise.reject(new Error(`Flow '${flowKey}' not found.`)); + } + + let lastResult: any; + let exited = false; + const stepResults: Record = {}; + const shared: Record = {}; + + // Create a new FlowContext instance for this flow execution + const createLogger = (level: string) => (message: string, meta?: any) => { + const logMessage = `[${level.toUpperCase()}] [Flow: ${flowKey}] [Model: ${this.uid}] ${message}`; + const logMeta = { flowKey, modelUid: this.uid, ...meta }; + console[level.toLowerCase()](logMessage, logMeta); + }; + + const globalContexts = currentFlowEngine.getContext() || {}; + const flowContext: FlowContext = { + exit: () => { + exited = true; + console.log(`Flow '${flowKey}' on model '${this.uid}' exited via ctx.exit().`); + }, + logger: { + info: createLogger('INFO'), + warn: createLogger('WARN'), + error: createLogger('ERROR'), + debug: createLogger('DEBUG'), + }, + stepResults, + shared, + globals: globalContexts, + extra: extra || {}, + model: this, + app: globalContexts.app || {}, + }; + + for (const stepKey in flow.steps) { + if (Object.prototype.hasOwnProperty.call(flow.steps, stepKey)) { + const step: StepDefinition = flow.steps[stepKey]; + if (exited) break; + + let handler: ((ctx: FlowContext, params: any) => Promise | any) | undefined; + let combinedParams: Record = {}; + let actionDefinition; + + if ((step as ActionStepDefinition).use) { + const actionStep = step as ActionStepDefinition; + actionDefinition = currentFlowEngine.getAction(actionStep.use); + if (!actionDefinition) { + console.error( + `BaseModel.applyFlow: Action '${actionStep.use}' not found for step '${stepKey}' in flow '${flowKey}'. Skipping.`, + ); + continue; + } + handler = actionDefinition.handler; + combinedParams = { ...actionDefinition.defaultParams, ...actionStep.defaultParams }; + } else if ((step as InlineStepDefinition).handler) { + const inlineStep = step as InlineStepDefinition; + handler = inlineStep.handler; + combinedParams = { ...inlineStep.defaultParams }; + } else { + console.error( + `BaseModel.applyFlow: Step '${stepKey}' in flow '${flowKey}' has neither 'use' nor 'handler'. Skipping.`, + ); + continue; + } + + const modelStepParams = this.getStepParams(flowKey, stepKey); + if (modelStepParams !== undefined) { + combinedParams = { ...combinedParams, ...modelStepParams }; + } + + try { + const currentStepResult = handler!(flowContext, combinedParams); + if (step.isAwait !== false) { + lastResult = await currentStepResult; + } else { + lastResult = currentStepResult; + } + + // Store step result + stepResults[stepKey] = lastResult; + } catch (error) { + console.error(`BaseModel.applyFlow: Error executing step '${stepKey}' in flow '${flowKey}':`, error); + return Promise.reject(error); + } + } + } + return Promise.resolve(lastResult); + } + + dispatchEvent(eventName: string, extra?: FlowExtraContext): void { + const currentFlowEngine = this.flowEngine; + if (!currentFlowEngine) { + console.warn('FlowEngine not available on this model for dispatchEvent. Please set flowEngine on the model.'); + return; + } + + // 获取所有流程 + const constructor = this.constructor as typeof FlowModel; + const allFlows = constructor.getFlows(); + + allFlows.forEach((flow) => { + if (flow.on && flow.on.eventName === eventName) { + console.log(`BaseModel '${this.uid}' dispatching event '${eventName}' to flow '${flow.key}'.`); + this.applyFlow(flow.key, extra).catch((error) => { + console.error( + `BaseModel.dispatchEvent: Error executing event-triggered flow '${flow.key}' for event '${eventName}':`, + error, + ); + }); + } + }); + } + + /** + * 创建一个新的 FlowModel 子类,并预注册指定的流程。 + * @param {ExtendedFlowDefinition[]} flows 要预注册的流程定义数组 + * 如果flow.patch为true,则表示这是对父类同名流程的部分覆盖 + * @returns 新创建的 FlowModel 子类 + */ + public static extends(this: T, flows: ExtendedFlowDefinition[] = []): T { + class CustomFlowModel extends (this as unknown as typeof FlowModel) { + // @ts-ignore + static name = `CustomFlowModel_${generateUid()}`; + } + + // 处理流程注册和覆盖 + if (flows.length > 0) { + flows.forEach((flowDefinition) => { + // 如果标记为部分覆盖,则调用extendFlow方法 + if (flowDefinition.patch === true) { + CustomFlowModel.extendFlow(flowDefinition); + } else { + // 完全覆盖或新增流程 + CustomFlowModel.registerFlow(flowDefinition as FlowDefinition); + } + }); + } + + return CustomFlowModel as unknown as T; + } + + /** + * 获取所有自动应用流程定义并按 sort 排序 + * @returns {FlowDefinition[]} 按 sort 排序的自动应用流程定义数组 + */ + public getAutoFlows(): FlowDefinition[] { + const constructor = this.constructor as typeof FlowModel; + const allFlows = constructor.getFlows(); + + // 过滤出自动流程并按 sort 排序 + const autoFlows = Array.from(allFlows.values()) + .filter((flow) => flow.auto === true) + .sort((a, b) => (a.sort || 0) - (b.sort || 0)); + + return autoFlows; + } + + /** + * 执行所有自动应用流程 + * @param {FlowExtraContext} [extra] 可选的额外上下文 + * @returns {Promise} 所有自动应用流程的执行结果数组 + */ + async applyAutoFlows(extra?: FlowExtraContext): Promise { + const autoApplyFlows = this.getAutoFlows(); + + if (autoApplyFlows.length === 0) { + console.warn(`FlowModel: No auto-apply flows found for model '${this.uid}'`); + return []; + } + + const results: any[] = []; + for (const flow of autoApplyFlows) { + try { + const result = await this.applyFlow(flow.key, extra); + results.push(result); + } catch (error) { + console.error(`FlowModel.applyAutoFlows: Error executing auto-apply flow '${flow.key}':`, error); + throw error; + } + } + + return results; + } + + /** + * Renders the React representation of this flow model. + * @returns {React.ReactNode} The React node to render. + */ + public render(): any { + // console.warn('FlowModel.render() not implemented. Override in subclass for FlowModelRenderer.'); + // 默认返回一个空的div,子类可以覆盖这个方法来实现具体的渲染逻辑 + return
; + } + + setParent(parent: FlowModel): void { + if (!parent || !(parent instanceof FlowModel)) { + throw new Error('Parent must be an instance of FlowModel.'); + } + this.parent = parent; + this.options.parentId = parent.uid; + } + + addSubModel(subKey: string, options: CreateModelOptions | FlowModel) { + let model: FlowModel; + if (options instanceof FlowModel) { + if (options.parent && options.parent !== this) { + throw new Error('Sub model already has a parent.'); + } + model = options; + } else { + model = this.flowEngine.createModel({ ...options, subKey, subType: 'array' }); + } + model.setParent(this); + Array.isArray(this.subModels[subKey]) || (this.subModels[subKey] = []); + this.subModels[subKey].push(model); + return model; + } + + setSubModel(subKey: string, options: CreateModelOptions | FlowModel) { + let model: FlowModel; + if (options instanceof FlowModel) { + if (options.parent && options.parent !== this) { + throw new Error('Sub model already has a parent.'); + } + model = options; + } else { + model = this.flowEngine.createModel({ ...options, parentId: this.uid, subKey, subType: 'object' }); + } + model.setParent(this); + this.subModels[subKey] = model; + return model; + } + + mapSubModels( + subKey: K, + callback: (model: ArrayElementType) => R, + ): R[] { + const model = this.subModels[subKey]; + + if (!model) { + return []; + } + + const results: R[] = []; + + _.castArray(model).forEach((item) => { + const result = (callback as (model: any) => R)(item); + results.push(result); + }); + + return results; + } + + createRootModel(options) { + return this.flowEngine.createModel(options); + } + + async applySubModelsAutoFlows(subKey: K, extra?: Record) { + await Promise.all( + this.mapSubModels(subKey, async (column) => { + await column.applyAutoFlows(extra); + }), + ); + } + + async save() { + if (!this.flowEngine) { + throw new Error('FlowEngine is not set on this model. Please set flowEngine before saving.'); + } + return this.flowEngine.saveModel(this); + } + + async destroy() { + if (!this.flowEngine) { + throw new Error('FlowEngine is not set on this model. Please set flowEngine before deleting.'); + } + + // 从 FlowEngine 中销毁模型 + this.flowEngine.destroyModel(this.uid); + + // 从父模型中移除当前模型的引用 + if (this.parent?.subModels) { + for (const subKey in this.parent.subModels) { + const subModelValue = this.parent.subModels[subKey]; + + if (Array.isArray(subModelValue)) { + const index = subModelValue.indexOf(this); + if (index !== -1) { + subModelValue.splice(index, 1); + break; + } + } else if (subModelValue === this) { + delete this.parent.subModels[subKey]; + break; + } + } + } + } + + /** + * 打开步骤设置对话框 + * 用于配置流程中特定步骤的参数和设置 + * @param {string} flowKey 流程的唯一标识符 + * @param {string} stepKey 步骤的唯一标识符 + * @returns {void} + */ + openStepSettingsDialog(flowKey: string, stepKey: string) { + return openStepSettingsDialogFn({ + model: this, + flowKey, + stepKey, + }); + } + + /** + * 配置必填步骤参数 + * 用于在一个分步表单中配置所有需要参数的步骤 + * @param {number | string} [dialogWidth=800] 对话框宽度,默认为800 + * @param {string} [dialogTitle='步骤参数配置'] 对话框标题,默认为'步骤参数配置' + * @returns {Promise} 返回表单提交的值 + */ + async configureRequiredSteps(dialogWidth?: number | string, dialogTitle?: string) { + return openRequiredParamsStepFormDialogFn({ + model: this, + dialogWidth, + dialogTitle, + }); + } + + // TODO: 不完整,需要考虑 sub-model 的情况 + serialize(): Record { + const data = { + uid: this.uid, + ..._.omit(this.options, ['flowEngine']), + props: this.props, + stepParams: this.stepParams, + }; + for (const subModelKey in this.subModels) { + data.subModels = data.subModels || {}; + if (Array.isArray(this.subModels[subModelKey])) { + data.subModels[subModelKey] = this.subModels[subModelKey].map((model: FlowModel) => model.serialize()); + } else if ((this.subModels[subModelKey] as any) instanceof FlowModel) { + data.subModels[subModelKey] = this.subModels[subModelKey].serialize(); + } + } + return data; + } +} + +export function defineFlow(definition: FlowDefinition): FlowDefinition { + return definition as FlowDefinition; +} diff --git a/packages/core/flow-engine/src/models/formBlockModel.ts b/packages/core/flow-engine/src/models/formBlockModel.ts new file mode 100644 index 0000000000..5c6621364a --- /dev/null +++ b/packages/core/flow-engine/src/models/formBlockModel.ts @@ -0,0 +1,119 @@ +/** + * 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 { DataBlockModel } from './dataBlockModel'; +import { SingleRecordResource } from '../resources'; +import { StepParams } from '../types'; + +// TODO: 未完成 + +export class FormBlockModel> extends DataBlockModel { + public declare resource: SingleRecordResource; + + constructor(options: { uid: string; stepParams?: StepParams; resource?: SingleRecordResource }) { + super({ + uid: options.uid, + stepParams: options.stepParams, + }); + if (options.resource) { + this.resource = options.resource; + } + } + + // 加载表单数据 + async load(params?: Record): Promise { + // TODO: 将 params 传递给 resource 的 refresh 方法 + console.log('FormBlockModel load 被调用,参数:', params); + await this.resource.refresh(); + return this.resource.getData(); + } + + // 重载表单数据 + async reload(): Promise { + await this.resource.refresh(); + return this.resource.getData(); + } + + // // 保存表单数据 + // async save(): Promise { + // try { + // // TODO: 调用 api 保存数据 + + // console.log('FormBlockModel save 被调用,数据:', this.resource.data); + // return true; + // } catch (error) { + // console.error('保存表单数据失败:', error); + // return false; + // } + // } + + // 重置表单数据 + reset(): void { + // TODO: 根据情况决定如何重置 + // 可能是清空数据 + // 或者重新加载初始数据 + this.resource.setData({} as TData); + // this.resource.refresh(); + } + + // 设置表单数据(部分或全部) + setFormData(data: Partial) { + const currentData = this.resource.getData(); + if (!currentData || Object.keys(currentData).length === 0) { + this.resource.setData(data as TData); + } else { + // TODO: 合并现有数据和新数据 + this.resource.setData({ + ...currentData, + ...data, + } as TData); + } + } + + // 获取表单数据 + getFormData(): TData { + return this.resource.getData(); + } + + // 获取表单值 + getFieldValue(fieldName: string): any { + const data = this.resource.getData(); + if (!data) return undefined; + return (data as any)[fieldName]; + } + + // 设置表单值 + setFieldValue(fieldName: string, value: any): void { + const currentData = this.resource.getData(); + if (!currentData || Object.keys(currentData).length === 0) { + const data: Record = {}; + data[fieldName] = value; + this.resource.setData(data as TData); + } else { + const updatedData = { ...(currentData as any) }; + updatedData[fieldName] = value; + this.resource.setData(updatedData as TData); + } + } + + getProps() { + return { + initialValues: this.resource.getData() || {}, + onValuesChange: (changedValues: any) => { + this.setFormData(changedValues as Partial); + }, + onSubmit: async (values: TData) => { + this.resource.setData(values); + await this.resource.save(values); + return true; + }, + fields: this.fields, + }; + } +} diff --git a/packages/core/flow-engine/src/models/index.ts b/packages/core/flow-engine/src/models/index.ts new file mode 100644 index 0000000000..19b66fd90c --- /dev/null +++ b/packages/core/flow-engine/src/models/index.ts @@ -0,0 +1,15 @@ +/** + * 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 './flowModel'; +export * from './blockModel'; +export * from './actions'; +export * from './dataBlockModel'; +export * from './tableBlockModel'; +export * from './formBlockModel'; diff --git a/packages/core/flow-engine/src/models/tableBlockModel.ts b/packages/core/flow-engine/src/models/tableBlockModel.ts new file mode 100644 index 0000000000..0b685fa350 --- /dev/null +++ b/packages/core/flow-engine/src/models/tableBlockModel.ts @@ -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 { DataBlockModel } from './dataBlockModel'; +import { ActionModel } from './actions/actionModel'; +import { observable } from '@formily/reactive'; +import { MultiRecordResource } from '../resources/multiRecordResource'; +import { StepParams } from '../types'; + +// TODO: 未完成 + +export class TableBlockModel extends DataBlockModel { + public rowActions: Map; + public declare resource: MultiRecordResource; + + constructor(options: { uid: string; stepParams?: StepParams; resource?: MultiRecordResource }) { + super({ + uid: options.uid, + stepParams: options.stepParams, + resource: options.resource, + }); + this.rowActions = observable(new Map()); + } + + setRowActions(actions: ActionModel[]) { + this.rowActions.clear(); + actions.forEach((action) => this.rowActions.set(action.uid, action)); + } + + addRowAction(action: ActionModel) { + this.rowActions.set(action.uid, action); + } + + getRowAction(uid: string): ActionModel | undefined { + return this.rowActions.get(uid); + } + + removeRowAction(uid: string): boolean { + return this.rowActions.delete(uid); + } + + getRowActions(): ActionModel[] { + return Array.from(this.rowActions.values()); + } + + async reload(): Promise { + await this.resource.refresh(); + return this.resource.getData(); + } + + async reset(): Promise { + this.resource.setPage(1); + this.resource.setFilter({}); + await this.resource.refresh(); + return this.resource.getData(); + } + + async applyFilter(filter: Record): Promise { + this.resource.setFilter(filter); + await this.resource.refresh(); + return this.resource.getData(); + } + + async applySort(field: string, direction: 'asc' | 'desc'): Promise { + // TODO: 需要在MultiRecordResource中添加排序支持 + await this.resource.refresh(); + return this.resource.getData(); + } +} diff --git a/packages/core/flow-engine/src/provider.tsx b/packages/core/flow-engine/src/provider.tsx new file mode 100644 index 0000000000..2d0e1e144e --- /dev/null +++ b/packages/core/flow-engine/src/provider.tsx @@ -0,0 +1,35 @@ +/** + * 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 } from 'react'; +import { FlowEngine } from './flowEngine'; + +interface FlowEngineProviderProps { + engine: FlowEngine; + children: React.ReactNode; +} + +const FlowEngineContext = createContext(null); + +export const FlowEngineProvider: React.FC = (props) => { + const { engine, children } = props; + return {children}; +}; + +export const useFlowEngine = (): FlowEngine => { + const context = useContext(FlowEngineContext); + if (!context) { + // This error should ideally not be hit if FlowEngineProvider is used correctly at the root + // and always supplied with an engine. + throw new Error( + 'useFlowEngine must be used within a FlowEngineProvider, and FlowEngineProvider must be supplied with an engine.', + ); + } + return context; +}; diff --git a/packages/core/flow-engine/src/resources/apiResource.ts b/packages/core/flow-engine/src/resources/apiResource.ts new file mode 100644 index 0000000000..8d5c2216f5 --- /dev/null +++ b/packages/core/flow-engine/src/resources/apiResource.ts @@ -0,0 +1,91 @@ +/** + * 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 { APIClient } from '@nocobase/sdk'; +import { FlowResource } from './flowResource'; + +export class APIResource extends FlowResource { + // 请求配置 + protected request: any = { + url: null as string | null, + method: 'get' as string, + params: {} as Record, + headers: {} as Record, + }; + + protected api: APIClient; + + setAPIClient(api: APIClient) { + this.api = api; + return this; + } + + getURL(): string { + return this.request.url; + } + + setURL(value: string) { + this.request.url = value; + return this; + } + + setRequestMethod(method: string) { + this.request.method = method; + return this; + } + + addRequestHeader(key: string, value: string) { + if (!this.request.headers) { + this.request.headers = {}; + } + this.request.headers[key] = value; + return this; + } + + addRequestParameter(key: string, value: any) { + if (!this.request.params) { + this.request.params = {}; + } + this.request.params[key] = value; + return this; + } + + setRequestBody(data: any) { + this.request.data = data; + return this; + } + + setRequestOptions(key: string, value: any) { + this.request[key] = value; + return this; + } + + async refresh() { + if (!this.api) { + throw new Error('API client not set'); + } + const { data } = await this.api.request({ + url: this.getURL(), + method: 'get', + ...this.getRefreshRequestOptions(), + }); + this.setData(data?.data); + } + + protected getRefreshRequestOptions(filterByTk?: string | number | string[] | number[]) { + const options = { + params: { ...this.request.params }, + headers: { ...this.request.headers }, + }; + if (filterByTk) { + options.params.filterByTk = filterByTk; + } + return options; + } +} diff --git a/packages/core/flow-engine/src/resources/baseRecordResource.ts b/packages/core/flow-engine/src/resources/baseRecordResource.ts new file mode 100644 index 0000000000..28360ff005 --- /dev/null +++ b/packages/core/flow-engine/src/resources/baseRecordResource.ts @@ -0,0 +1,183 @@ +/** + * 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 { APIResource } from './apiResource'; + +export abstract class BaseRecordResource extends APIResource { + protected resourceName: string | null = null; + protected sourceId: string | number | null = null; + + // 请求配置 - 与 APIClient 接口保持一致 + protected request = { + url: null as string | null, + method: 'get' as string, + params: { + filter: {} as Record, + filterByTk: null as string | number | string[] | number[] | null, + appends: null as string[] | null, + fields: null as string[] | null, + sort: null as string[] | null, + except: null as string[] | null, + whitelist: null as string[] | null, + blacklist: null as string[] | null, + } as Record, + headers: {} as Record, + }; + + protected splitValue(value: string | string[]): string[] { + if (typeof value === 'string') { + return value.split(',').map((item) => item.trim()); + } + return Array.isArray(value) ? value : []; + } + + protected buildURL(action?: string): string { + let url = ''; + if (this.resourceName) { + if (this.sourceId && this.resourceName.includes('.')) { + // 处理关联资源,如 users.tags 或 users.profile + const [parentResource, childResource] = this.resourceName.split('.'); + url = `${parentResource}/${this.sourceId}/${childResource}:${action || 'get'}`; + } else { + url = `${this.resourceName}:${action || 'get'}`; + } + } + + return url; + } + + async runAction(action: string, options: any) { + const { data } = await this.api.request({ + url: this.buildURL(action), + method: 'post', + ...options, + }); + if (!data?.data) { + return data; + } + return { data: data.data, meta: data.meta } as { + data: TData; + meta?: TMeta; + }; + } + + setResourceName(resourceName: string) { + this.resourceName = resourceName; + return this; + } + + getResourceName(): string { + return this.resourceName; + } + + setSourceId(sourceId: string | number) { + this.sourceId = sourceId; + return this; + } + + getSourceId(): string | number { + return this.sourceId; + } + + setDataSourceKey(dataSourceKey: string) { + return this.addRequestHeader('X-Data-Source', dataSourceKey); + } + + getDataSourceKey(): string { + return this.request.headers['X-Data-Source']; + } + + setFilter(filter: Record) { + return this.addRequestParameter('filter', filter); + } + + getFilter(): Record { + return this.request.params.filter; + } + + setAppends(appends: string[]) { + return this.addRequestParameter('appends', appends); + } + + getAppends(): string[] { + return this.request.params.appends || []; + } + + addAppends(appends: string | string[]) { + const currentAppends = this.getAppends(); + const newAppends = this.splitValue(appends); + newAppends.forEach((append) => { + if (!currentAppends.includes(append)) { + currentAppends.push(append); + } + }); + this.request.params.appends = currentAppends; + return this; + } + + removeAppends(appends: string | string[]) { + if (!this.request.params.appends) { + return this; + } + const currentAppends = this.getAppends(); + const removeAppends = this.splitValue(appends); + this.request.params.appends = currentAppends.filter((append: string) => !removeAppends.includes(append)); + return this; + } + + setFilterByTk(filterByTk: string | number | string[] | number[]) { + return this.addRequestParameter('filterByTk', filterByTk); + } + + getFilterByTk(): string | number | string[] | number[] { + return this.request.params.filterByTk; + } + + setFields(fields: string[] | string) { + return this.addRequestParameter('fields', this.splitValue(fields)); + } + + getFields(): string[] { + return this.request.params.fields || []; + } + + setSort(sort: string | string[]) { + return this.addRequestParameter('fields', this.splitValue(sort)); + } + + getSort(): string[] { + return this.request.params.sort || []; + } + + setExcept(except: string | string[]) { + return this.addRequestParameter('except', this.splitValue(except)); + } + + getExcept(): string[] { + return this.request.params.except || []; + } + + setWhitelist(whitelist: string | string[]) { + return this.addRequestParameter('whitelist', this.splitValue(whitelist)); + } + + getWhitelist(): string[] { + return this.request.params.whitelist || []; + } + + setBlacklist(blacklist: string | string[]) { + return this.addRequestParameter('blacklist', this.splitValue(blacklist)); + } + + getBlacklist(): string[] { + return this.request.params.blacklist || []; + } + + abstract refresh(): Promise; +} diff --git a/packages/core/flow-engine/src/resources/flowResource.ts b/packages/core/flow-engine/src/resources/flowResource.ts new file mode 100644 index 0000000000..d35b6ad3c6 --- /dev/null +++ b/packages/core/flow-engine/src/resources/flowResource.ts @@ -0,0 +1,33 @@ +/** + * 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 { observable } from '@formily/reactive'; + +export class FlowResource { + protected _data = observable.ref(null); + protected _meta = observable.ref>({}); + + getData(): TData { + return this._data.value; + } + + setData(value: TData) { + this._data.value = value; + return this; + } + + getMeta(metaKey?: string) { + return metaKey ? this._meta.value[metaKey] : this._meta.value; + } + + setMeta(meta: Record) { + this._meta.value = { ...this._meta.value, ...meta }; + return this; + } +} diff --git a/packages/core/flow-engine/src/resources/index.ts b/packages/core/flow-engine/src/resources/index.ts new file mode 100644 index 0000000000..953edffc1b --- /dev/null +++ b/packages/core/flow-engine/src/resources/index.ts @@ -0,0 +1,13 @@ +/** + * 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 './flowResource'; +export * from './apiResource'; +export * from './singleRecordResource'; +export * from './multiRecordResource'; diff --git a/packages/core/flow-engine/src/resources/multiRecordResource.ts b/packages/core/flow-engine/src/resources/multiRecordResource.ts new file mode 100644 index 0000000000..3bd81b92e0 --- /dev/null +++ b/packages/core/flow-engine/src/resources/multiRecordResource.ts @@ -0,0 +1,119 @@ +/** + * 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 { observable } from '@formily/reactive'; +import { BaseRecordResource } from './baseRecordResource'; + +export class MultiRecordResource extends BaseRecordResource { + protected _data = observable.ref([]); + + // 请求配置 - 与 APIClient 接口保持一致 + protected request = { + url: null as string | null, + method: 'get' as string, + params: { + filter: {} as Record, + filterByTk: null as string | number | string[] | number[] | null, + appends: [] as string[], + fields: [] as string[], + sort: null as string[] | null, + except: null as string[] | null, + whitelist: null as string[] | null, + blacklist: null as string[] | null, + page: 1 as number, + pageSize: 20 as number, + } as Record, + headers: {} as Record, + }; + + async next(): Promise { + this.request.params.page += 1; + await this.refresh(); + } + + async previous(): Promise { + if (this.request.params.page > 1) { + this.request.params.page -= 1; + await this.refresh(); + } + } + + async goto(page: number): Promise { + if (page > 0) { + this.request.params.page = page; + await this.refresh(); + } + } + + async create(data: TDataItem): Promise { + await this.runAction('create', { + data, + }); + await this.refresh(); + } + + async update(filterByTk: string | number, data: Partial): Promise { + const options = { + params: { + filterByTk, + }, + headers: this.request.headers, + }; + await this.runAction('update', { + ...options, + data, + }); + await this.refresh(); + } + + async destroy(filterByTk: string | number | string[] | number[]): Promise { + const options = { + params: { + filterByTk, + }, + headers: this.request.headers, + }; + await this.runAction('destroy', { + ...options, + }); + await this.refresh(); + } + + setPage(page: number) { + this.request.params.page = page; + return this; + } + + getPage(): number { + return this.request.params.page; + } + + setPageSize(pageSize: number) { + this.request.params.pageSize = pageSize; + return this; + } + + getPageSize(): number { + return this.request.params.pageSize; + } + + async refresh(): Promise { + const { data, meta } = await this.runAction('list', { + ...this.getRefreshRequestOptions(), + method: 'get', + }); + this.setData(data).setMeta(meta); + if (meta?.page) { + this.setPage(meta.page); + } + if (meta?.pageSize) { + this.setPageSize(meta.pageSize); + } + } +} diff --git a/packages/core/flow-engine/src/resources/singleRecordResource.ts b/packages/core/flow-engine/src/resources/singleRecordResource.ts new file mode 100644 index 0000000000..128442f51f --- /dev/null +++ b/packages/core/flow-engine/src/resources/singleRecordResource.ts @@ -0,0 +1,56 @@ +/** + * 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 { observable } from '@formily/reactive'; +import { BaseRecordResource } from './baseRecordResource'; + +export class SingleRecordResource extends BaseRecordResource { + setFilterByTk(filterByTk: string | number) { + this.addRequestParameter('filterByTk', filterByTk); + return this; + } + + async save(data: TData): Promise { + const options: any = { + headers: this.request.headers, + params: {}, + }; + let actionName = 'create'; + if (this.request.params.filterByTk) { + options.params.filterByTk = this.request.params.filterByTk; + actionName = 'update'; + } + await this.runAction(actionName, { + ...options, + data, + }); + await this.refresh(); + } + + async destroy(): Promise { + const options: any = { + headers: this.request.headers, + params: {}, + }; + options.params.filterByTk = this.request.params.filterByTk; + await this.runAction('destroy', { + ...options, + }); + this.setData(null); + } + + async refresh(): Promise { + const options: any = this.getRefreshRequestOptions(); + const { data, meta } = await this.runAction('get', { + ...options, + method: 'get', + }); + this.setData(data).setMeta(meta); + } +} diff --git a/packages/core/flow-engine/src/types.ts b/packages/core/flow-engine/src/types.ts new file mode 100644 index 0000000000..a4fca59be9 --- /dev/null +++ b/packages/core/flow-engine/src/types.ts @@ -0,0 +1,311 @@ +/** + * 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 { ISchema } from '@formily/json-schema'; +import type { FlowEngine } from './flowEngine'; +import type { FlowModel } from './models'; + +/** + * 工具类型:如果 T 是数组类型,则提取数组元素类型;否则返回 T 本身 + * @template T 要检查的类型 + * @example + * ```typescript + * type Test1 = ArrayElementType; // string + * type Test2 = ArrayElementType; // number + * type Test3 = ArrayElementType; // string + * type Test4 = ArrayElementType<{ id: number }[]>; // { id: number } + * type Test5 = ArrayElementType<{ id: number }>; // { id: number } + * ``` + */ +export type ArrayElementType = T extends (infer U)[] ? U : T; + +export type DeepPartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? DeepPartial[] + : T[P] extends Record + ? DeepPartial + : T[P] extends object + ? DeepPartial + : T[P]; +}; + +/** + * Defines a flow with generic model type support. + */ +export interface FlowDefinition { + key: string; // Unique identifier for the flow + title?: string; + /** + * Whether this flow is a default flow that should be automatically executed + */ + auto?: boolean; + /** + * Sort order for flow execution, lower numbers execute first + * Defaults to 0, can be negative + */ + sort?: number; + /** + * Optional configuration to allow this flow to be triggered by `dispatchEvent`. + */ + on?: { + eventName: string; + }; + steps: Record>; +} + +// 扩展FlowDefinition类型,添加partial标记用于部分覆盖 +export interface ExtendedFlowDefinition extends DeepPartial { + /** + * Whether this flow is a default flow that should be automatically executed + */ + auto?: boolean; + /** + * Sort order for flow execution, lower numbers execute first + * Defaults to 0, can be negative + */ + sort?: number; + /** + * 当设置为true时,表示这是对现有流程的部分覆盖,而不是完全替换 + * 如果父类中不存在同名流程,此标记将被忽略 + */ + patch?: boolean; +} + +export interface IModelComponentProps { + [key: string]: any; +} + +// 定义只读版本的props类型 +export type ReadonlyModelProps = Readonly; + +/** + * Context object passed to handlers during flow execution. + */ +export interface FlowContext { + exit: () => void; // Terminate the entire flow execution + logger: { + info: (message: string, meta?: any) => void; + warn: (message: string, meta?: any) => void; + error: (message: string, meta?: any) => void; + debug: (message: string, meta?: any) => void; + }; + stepResults: Record; // Results from previous steps + shared: Record; // Shared data within the flow (read/write) + globals: Record; // Global context data (read-only) + extra: Record; // Extra context passed to applyFlow (read-only) + model: TModel; // Current model instance with specific type + app: any; // Application instance (required) +} + +export type CreateSubModelOptions = CreateModelOptions | FlowModel; + +/** + * Constructor for model classes. + */ +export type ModelConstructor = new (options: { + uid: string; + props?: IModelComponentProps; + stepParams?: StepParams; + meta?: FlowModelMeta; + subModels?: Record; + [key: string]: any; // Allow additional options +}) => T; + +/** + * Defines a reusable action with generic model type support. + */ +export interface ActionDefinition { + name: string; // Unique identifier for the action + title?: string; + handler: (ctx: FlowContext, params: any) => Promise | any; + uiSchema?: Record; + defaultParams?: Record; +} + +/** + * Base interface for a step definition with generic model type support. + */ +interface BaseStepDefinition { + title?: string; + isAwait?: boolean; // Whether to await the handler, defaults to true +} + +/** + * Step that uses a registered Action with generic model type support. + */ +export interface ActionStepDefinition extends BaseStepDefinition { + use: string; // Name of the registered ActionDefinition + uiSchema?: Record; // Optional: overrides uiSchema from ActionDefinition + defaultParams?: Record; // Optional: overrides/extends defaultParams from ActionDefinition + paramsRequired?: boolean; // Optional: whether the step params are required, will open the config dialog before adding the model + hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu + // Cannot have its own handler + handler?: undefined; +} + +/** + * Step that defines its handler inline with generic model type support. + */ +export interface InlineStepDefinition extends BaseStepDefinition { + handler: (ctx: FlowContext, params: any) => Promise | any; + uiSchema?: Record; // Optional: uiSchema for this inline step + defaultParams?: Record; // Optional: defaultParams for this inline step + paramsRequired?: boolean; // Optional: whether the step params are required, will open the config dialog before adding the model + hideInSettings?: boolean; // Optional: whether to hide the step in the settings menu + // Cannot use a registered action + use?: undefined; +} + +export type StepDefinition = + | ActionStepDefinition + | InlineStepDefinition; + +/** + * Extra context for flow execution - represents the data that will appear in ctx.extra + * This is the type for data passed to applyFlow that becomes ctx.extra + */ +export type FlowExtraContext = Record; + +/** + * Action options for registering actions with generic model type support + */ +export interface ActionOptions { + handler: (ctx: FlowContext, params: P) => Promise | R; + uiSchema?: Record; + defaultParams?: Partial

; +} + +/** + * Steps parameters structure for flow models + * + * @example + * ```typescript + * const stepParams: StepParams = { + * 'flow1': { + * 'step1': { + * 'param1': 'value1', + * 'param2': 'value2' + * }, + * 'step2': { + * 'param3': 'value3' + * } + * }, + * 'flow2': { + * 'step1': { + * 'param1': 'value1' + * } + * } + * } + * ``` + */ +export type StepParams = { + [flowKey: string]: { + [stepKey: string]: { + [paramKey: string]: any; + }; + }; +}; + +/** + * 已注册模型的类名 + */ +export type RegisteredModelClassName = string; + +/** + * Options for creating a model instance + */ +export interface CreateModelOptions { + uid?: string; + use: RegisteredModelClassName | ModelConstructor; + props?: IModelComponentProps; + stepParams?: StepParams; + subModels?: Record; + parentId?: string; + subKey?: string; + subType?: 'object' | 'array'; + [key: string]: any; // 允许额外的自定义选项 +} +export interface IFlowModelRepository { + load(uid: string): Promise | null>; + save(model: T): Promise>; + destroy(uid: string): Promise; +} + +/** + * 步骤设置对话框的属性接口 + */ +export interface StepSettingsDialogProps { + model: any; + flowKey: string; + stepKey: string; + dialogWidth?: number | string; + dialogTitle?: string; +} + +/** + * 分步表单对话框的属性接口 + */ +export interface RequiredConfigStepFormDialogProps { + model: any; + dialogWidth?: number | string; + dialogTitle?: string; +} + +export type SubModelValue = TModel | TModel[]; + +export interface DefaultStructure { + parent?: any, + subModels?: Record +} + +/** + * Options for FlowModel constructor + */ +export interface FlowModelOptions { + uid: string; + props?: IModelComponentProps; + stepParams?: Record; + subModels?: Structure['subModels']; + flowEngine: FlowEngine; + parentId?: string; + subKey?: string; + subType?: 'object' | 'array'; +} + +export interface FlowModelMeta { + title: string; + group?: string; + defaultOptions?: Record; + icon?: string; + // uniqueSub?: boolean; + /** + * 排序权重,数字越小排序越靠前,用于控制显示顺序和默认选择 + * 排序最靠前的将作为默认选择 + * @default 0 + */ + sort?: number; +} + +/** + * 字段 FlowModel 的专用元数据接口 + * 继承自 FlowModelMeta,添加了字段接口相关的属性 + */ +export interface FieldFlowModelMeta extends FlowModelMeta { + /** + * 支持的字段接口组列表,基于 CollectionFieldInterface 的 group 属性 + * 如:['basic', 'choices', 'relation'] 等 + * 如果不指定,则支持所有接口(向后兼容) + */ + supportedInterfaceGroups?: string[]; + /** + * 支持的具体接口列表(可选,用于更精确的控制) + * 如:['input', 'textarea', 'select'] 等 + */ + supportedInterfaces?: string[]; +} \ No newline at end of file diff --git a/packages/core/flow-engine/src/utils.ts b/packages/core/flow-engine/src/utils.ts new file mode 100644 index 0000000000..f68a1588e7 --- /dev/null +++ b/packages/core/flow-engine/src/utils.ts @@ -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. + */ + +export function generateUid(): string { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +} +import type { FlowDefinition } from './types'; +import _ from 'lodash'; +import { DeepPartial, ModelConstructor } from './types'; + +/** + * 合并两个流程定义 + * 用于处理流程的部分覆盖(patch)场景 + * + * @param originalFlow 原始流程定义 + * @param patchFlow 覆盖流程定义 + * @returns 合并后的流程定义 + */ +export function mergeFlowDefinitions( + originalFlow: FlowDefinition, + patchFlow: DeepPartial, +): FlowDefinition { + // 创建新的流程定义,合并原始流程和覆盖配置 + const flow = _.cloneDeep(originalFlow); + const mergedFlow = { + ...flow, + title: patchFlow.title ?? flow.title, + ...(patchFlow.on && { on: patchFlow.on as { eventName: string } }), + ...flow.steps, + } as FlowDefinition; + + // 覆盖特定步骤 + if (patchFlow.steps) { + Object.entries(patchFlow.steps).forEach(([stepKey, stepDefinition]) => { + mergedFlow.steps[stepKey] = _.merge(flow.steps[stepKey], stepDefinition); + }); + } + + return mergedFlow; +} + +/** + * 检查一个类是否继承自指定的父类(支持多层继承) + * @param {ModelConstructor} childClass 要检查的子类 + * @param {ModelConstructor} parentClass 父类 + * @returns {boolean} 如果子类继承自父类则返回 true + */ +export function isInheritedFrom(childClass: ModelConstructor, parentClass: ModelConstructor): boolean { + // 如果是同一个类,返回 false(不包括自身) + if (childClass === parentClass) { + return false; + } + + // 检查直接继承 + if (childClass.prototype instanceof parentClass) { + return true; + } + + // 递归检查原型链 + let currentProto = Object.getPrototypeOf(childClass.prototype); + while (currentProto && currentProto !== Object.prototype) { + if (currentProto.constructor === parentClass) { + return true; + } + currentProto = Object.getPrototypeOf(currentProto); + } + + return false; +} diff --git a/packages/core/flow-engine/src/withFlowModel.tsx b/packages/core/flow-engine/src/withFlowModel.tsx new file mode 100644 index 0000000000..f678a522ad --- /dev/null +++ b/packages/core/flow-engine/src/withFlowModel.tsx @@ -0,0 +1,229 @@ +/** + * 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'; +import { observer } from '@formily/react'; +import { FlowModel } from './models'; +import { useApplyAutoFlows } from './hooks/useApplyFlow'; +import { useFlowExtraContext } from './hooks/useFlowExtraContext'; +import { useFlowModel } from './hooks/useFlowModel'; + +// 基础组件props类型 +type BaseFlowModelRendererProps

> = { + model?: FlowModel; +} & { + [key in keyof P]?: P[key]; +}; + +// 当不提供model创建选项时的props类型,model 必须提供且类型为 FlowModel +type PropsWithRequiredModel

> = { + model: FlowModel; +} & { + [key in keyof P]?: P[key]; +}; + +// 当提供model创建选项时的props类型,model 不能提供(类型为never) +type PropsWithNeverModel

> = { + model?: never; +} & { + [key in keyof P]?: P[key]; +}; + +// HOC 选项接口,使用条件类型确保正确的依赖关系 +interface WithFlowModelOptionsBase { + settings?: { + component?: React.ComponentType<{ + model?: FlowModel; + uid?: string; + children?: React.ReactNode; + [key: string]: any; + }>; // 设置组件,作为wrapper + props?: Record; // 传递给设置组件的参数 + }; +} + +// 当提供use时,uid可选 +interface WithFlowModelOptionsWithUse extends WithFlowModelOptionsBase { + use: string; // 模型类名,如 'MarkdownModel' + uid?: string; // 模型uid,可选 +} + +// 当不提供use但提供uid时,use必填 +interface WithFlowModelOptionsWithUid extends WithFlowModelOptionsBase { + use: string; // 模型类名,当提供uid时必填 + uid: string; // 模型uid +} + +// 当都不提供时 +interface WithFlowModelOptionsWithoutModel extends WithFlowModelOptionsBase { + use?: never; + uid?: never; +} + +// 联合类型 +type WithFlowModelOptions = + | WithFlowModelOptionsWithUse + | WithFlowModelOptionsWithUid + | WithFlowModelOptionsWithoutModel; + +// 当已经有模型时的组件 +function WithExistingModel

({ + model, + WrappedComponent, + options, + ...restProps +}: { + model: FlowModel; + WrappedComponent: React.ComponentType

; + options?: WithFlowModelOptions; +} & P) { + const extraContext = useFlowExtraContext(); + + // 始终应用默认流程 + useApplyAutoFlows(model, extraContext); + + const modelProps = model?.getProps(); + const combinedProps = { ...restProps, ...modelProps } as unknown as P; + + const wrappedElement = ; + + // 如果有设置组件,使用设置组件作为wrapper包装原始组件 + if (options?.settings?.component) { + const SettingsComponent = options.settings.component; + const settingsProps = { + model, + children: wrappedElement, + ...options.settings.props, + }; + + return ; + } else { + // 如果没有设置组件,直接返回原始组件 + return wrappedElement; + } +} + +// 当需要创建模型时的组件 +function WithCreatedModel

({ + uid, + use, + WrappedComponent, + options, + ...restProps +}: { + uid: string; + use: string; + WrappedComponent: React.ComponentType

; + options?: WithFlowModelOptions; +} & P) { + const extraContext = useFlowExtraContext(); + + // 使用 useFlowModel 创建模型 + const model = useFlowModel(uid, use); + + // 始终应用默认流程 + useApplyAutoFlows(model, extraContext); + + const modelProps = model?.getProps(); + const combinedProps = { ...restProps, ...modelProps } as unknown as P; + + const wrappedElement = ; + + // 如果有设置组件,使用设置组件作为wrapper包装原始组件 + if (options?.settings?.component) { + const SettingsComponent = options.settings.component; + const settingsProps = { + model, + children: wrappedElement, + ...options.settings.props, + }; + + return ; + } else { + // 如果没有设置组件,直接返回原始组件 + return wrappedElement; + } +} + +// 内部组件实现 - 根据条件渲染不同的组件 +function WithFlowModelInternal

( + props: BaseFlowModelRendererProps

, + WrappedComponent: React.ComponentType

, + options?: WithFlowModelOptions, +) { + const { model: propModel, ...restPassthroughProps } = props; + + // 如果已经有模型,使用现有模型组件 + if (propModel) { + // 当提供了 use 和 uid 时,不应该同时提供 model + if (options?.use && options?.uid) { + throw new Error( + 'Cannot provide both model prop and use/uid options. When use and uid are provided, model should not be provided.', + ); + } + + return ( + + ); + } + + // 如果需要创建模型且提供了必要参数 + if (options?.use && options?.uid) { + return ( + + ); + } + + // 如果既没有模型也没有创建参数,抛出错误 + throw new Error('Model is required. Either provide model prop or specify use and uid in options.'); +} + +// HOC函数重载,提供不同的类型签名 +export function withFlowModel

( + WrappedComponent: React.ComponentType

, + options: WithFlowModelOptionsWithUse & { uid: string }, +): React.ComponentType>; + +export function withFlowModel

( + WrappedComponent: React.ComponentType

, + options: WithFlowModelOptionsWithUid, +): React.ComponentType>; + +export function withFlowModel

( + WrappedComponent: React.ComponentType

, + options: WithFlowModelOptionsWithUse & { uid?: undefined }, +): React.ComponentType>; + +export function withFlowModel

( + WrappedComponent: React.ComponentType

, + options?: WithFlowModelOptionsWithoutModel, +): React.ComponentType>; + +export function withFlowModel

( + WrappedComponent: React.ComponentType

, + options?: WithFlowModelOptions, +) { + const WithFlowModel = observer((props: BaseFlowModelRendererProps

) => + WithFlowModelInternal(props, WrappedComponent, options), + ); + + WithFlowModel.displayName = `WithFlowModel(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; + return WithFlowModel; +} diff --git a/packages/plugins/@nocobase/plugin-block-cloud/LICENSE b/packages/plugins/@nocobase/plugin-block-cloud/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/plugins/@nocobase/plugin-block-cloud/README.md b/packages/plugins/@nocobase/plugin-block-cloud/README.md new file mode 100644 index 0000000000..648d8fe16f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/README.md @@ -0,0 +1,71 @@ +# Cloud Component Block Plugin + +A NocoBase plugin that enables creating cloud component blocks that can load external JavaScript libraries and CSS files, then render components using custom adapter code. + +## Features + +- **Dynamic Library Loading**: Load external JavaScript libraries via CDN using app.requirejs +- **CSS Support**: Load external CSS files for styling +- **Custom Adapter Code**: Write custom JavaScript code to render components using the loaded libraries +- **Flow Page Integration**: Automatically appears in the "Add Block" dropdown in flow pages + +## Configuration + +Each cloud component block has three main configurations: + +1. **JS CDN URL**: The URL to the JavaScript library (e.g., `https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js`) +2. **CSS URL** (Optional): The URL to the CSS file for styling +3. **Adapter Code**: Custom JavaScript code that uses the loaded library to render components + +## Example Usage + +### ECharts Example + +**JS CDN URL**: `https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js` +**Library Name**: `echarts` +**Adapter Code**: +```javascript +// Available variables: +// - element: The DOM element to render into +// - echarts: The loaded ECharts library +// - ctx: Flow context +// - model: Current model instance + +const chart = echarts.init(element); +const option = { + title: { text: 'Cloud Component Demo' }, + tooltip: {}, + xAxis: { data: ['A', 'B', 'C', 'D', 'E'] }, + yAxis: {}, + series: [{ + name: 'Demo', + type: 'bar', + data: [5, 20, 36, 10, 10] + }] +}; +chart.setOption(option); +``` + +## Installation + +1. Place the plugin in `packages/plugins/@nocobase/plugin-block-cloud/` +2. Install dependencies: `npm install` +3. Build the plugin: `npm run build` +4. Enable the plugin in NocoBase admin panel + +## Development + +The plugin follows the standard NocoBase plugin structure: + +- `src/server/`: Server-side plugin code +- `src/client/`: Client-side plugin code +- `src/client/CloudBlockFlowModel.tsx`: Main flow model implementation + +## Architecture + +The plugin extends the `BlockFlowModel` class and implements a default flow with two steps: + +1. **loadLibrary**: Configures requirejs and loads the external JavaScript/CSS +2. **setupComponent**: Executes the adapter code to render the component + +The plugin automatically registers with the flow engine and appears in the flow page's "Add Block" dropdown. diff --git a/packages/plugins/@nocobase/plugin-block-cloud/build.config.ts b/packages/plugins/@nocobase/plugin-block-cloud/build.config.ts new file mode 100644 index 0000000000..b79d40bc8d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/build.config.ts @@ -0,0 +1,16 @@ +/** + * 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 { defineConfig } from '@nocobase/build'; + +export default defineConfig({ + afterBuild: (log) => { + log('Custom build step for plugin-block-cloud'); + }, +}); diff --git a/packages/plugins/@nocobase/plugin-block-cloud/client.d.ts b/packages/plugins/@nocobase/plugin-block-cloud/client.d.ts new file mode 100644 index 0000000000..896ba758bb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/client.d.ts @@ -0,0 +1 @@ +export * from './dist/client'; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/client.js b/packages/plugins/@nocobase/plugin-block-cloud/client.js new file mode 100644 index 0000000000..b6e3be70e6 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/client.js @@ -0,0 +1 @@ +module.exports = require('./dist/client/index.js'); diff --git a/packages/plugins/@nocobase/plugin-block-cloud/package.json b/packages/plugins/@nocobase/plugin-block-cloud/package.json new file mode 100644 index 0000000000..1be63d28af --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/package.json @@ -0,0 +1,30 @@ +{ + "name": "@nocobase/plugin-block-cloud", + "displayName": "Block: Cloud Component", + "displayName.zh-CN": "区块:云组件", + "description": "Create cloud component blocks that load external JS/CSS and render components via adapter code.", + "description.zh-CN": "创建云组件区块,加载外部JS/CSS并通过适配器代码渲染组件。", + "version": "1.8.0-beta.4", + "license": "AGPL-3.0", + "main": "./dist/server/index.js", + "types": "./dist/index.d.ts", + "devDependencies": { + "@ant-design/icons": "5.x", + "@formily/react": "2.x", + "@formily/shared": "2.x", + "antd": "5.x", + "react": "^18.2.0", + "react-i18next": "^11.15.1" + }, + "peerDependencies": { + "@nocobase/client": "workspace:*", + "@nocobase/server": "workspace:*" + }, + "keywords": [ + "nocobase", + "plugin", + "block", + "cloud", + "component" + ] +} diff --git a/packages/plugins/@nocobase/plugin-block-cloud/server.d.ts b/packages/plugins/@nocobase/plugin-block-cloud/server.d.ts new file mode 100644 index 0000000000..699fd532ab --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/server.d.ts @@ -0,0 +1 @@ +export * from './dist/server'; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/server.js b/packages/plugins/@nocobase/plugin-block-cloud/server.js new file mode 100644 index 0000000000..972842039a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/server.js @@ -0,0 +1 @@ +module.exports = require('./dist/server/index.js'); diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/client.ts new file mode 100644 index 0000000000..c445bd2e15 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client.ts @@ -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 { default } from './client/index'; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx new file mode 100644 index 0000000000..c2bc646226 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client/CloudBlockFlowModel.tsx @@ -0,0 +1,230 @@ +/** + * 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, Spin } from 'antd'; +import React, { createRef } from 'react'; +import { BlockFlowModel } from '@nocobase/client'; + +function waitForRefCallback(ref: React.RefObject, 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 CloudBlockFlowModel extends BlockFlowModel { + ref = createRef(); + + render() { + const { loading, error } = this.props; + + if (error) { + return ( + +

Error loading cloud component: {error}
+ + ); + } + + return ( + + {loading && ( +
+ +
Loading cloud component...
+
+ )} +
+ + ); + } +} + +CloudBlockFlowModel.define({ + title: 'Cloud Component', + group: 'Content', + icon: 'CloudOutlined', + defaultOptions: { + use: 'CloudBlockFlowModel', + stepParams: { + default: { + loadLibrary: { + jsUrl: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js', + cssUrl: '', + libraryName: 'echarts', + }, + setupComponent: { + adapterCode: ` +// Example adapter code for ECharts +const chart = echarts.init(element); +const option = { + title: { text: 'Cloud Component Demo' }, + tooltip: {}, + xAxis: { data: ['A', 'B', 'C', 'D', 'E'] }, + yAxis: {}, + series: [{ + name: 'Demo', + type: 'bar', + data: [5, 20, 36, 10, 10] + }] +}; +chart.setOption(option); + `.trim(), + }, + }, + }, + }, +}); + +CloudBlockFlowModel.registerFlow({ + key: 'default', + auto: true, + steps: { + loadLibrary: { + uiSchema: { + jsUrl: { + type: 'string', + title: 'JS CDN URL', + 'x-component': 'Input', + 'x-component-props': { + placeholder: 'https://cdn.jsdelivr.net/npm/library@version/dist/library.min.js', + }, + }, + cssUrl: { + type: 'string', + title: 'CSS URL (Optional)', + 'x-component': 'Input', + 'x-component-props': { + placeholder: 'https://cdn.jsdelivr.net/npm/library@version/dist/library.min.css', + }, + }, + libraryName: { + type: 'string', + title: 'Library Name', + 'x-component': 'Input', + 'x-component-props': { + placeholder: 'echarts', + }, + }, + }, + defaultParams: { + jsUrl: 'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js', + cssUrl: '', + libraryName: 'echarts', + }, + async handler(ctx: any, params: any) { + ctx.model.setProps('loading', true); + ctx.model.setProps('error', null); + + try { + // Configure requirejs paths + const paths: Record = {}; + paths[params.libraryName] = params.jsUrl.replace(/\.js$/, ''); + + // Load CSS if provided + if (params.cssUrl) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = params.cssUrl; + document.head.appendChild(link); + } + + // Configure requirejs + // const requireAsync = async (mod: string): Promise => { + // return new Promise((resolve, reject) => { + // ctx.app.requirejs.requirejs([mod], (arg: any) => resolve(arg), reject); + // }); + // }; + + ctx.app.requirejs.requirejs.config({ paths }); + await ctx.globals.requireAsync(params.libraryName); + + // Return the library name for the next step + return { libraryName: params.libraryName }; + } catch (error: any) { + ctx.model.setProps('error', error.message); + ctx.model.setProps('loading', false); + throw error; + } + }, + }, + setupComponent: { + uiSchema: { + adapterCode: { + type: 'string', + title: 'Adapter Code', + 'x-component': 'Input.TextArea', + 'x-component-props': { + autoSize: { minRows: 10, maxRows: 20 }, + placeholder: `// Write your adapter code here +// Available variables: +// - element: The DOM element to render into +// - library: The loaded library (e.g., echarts) +// - ctx: Flow context +// - model: Current model instance + +const chart = library.init(element); +chart.setOption({ + // your chart configuration +});`, + }, + }, + }, + defaultParams: { + adapterCode: ` +// Example adapter code for ECharts +const chart = echarts.init(element); +const option = { + title: { text: 'Cloud Component Demo' }, + tooltip: {}, + xAxis: { data: ['A', 'B', 'C', 'D', 'E'] }, + yAxis: {}, + series: [{ + name: 'Demo', + type: 'bar', + data: [5, 20, 36, 10, 10] + }] +}; +chart.setOption(option); + `.trim(), + }, + async handler(ctx: any, params: any) { + const { libraryName } = ctx.stepResults.loadLibrary || {}; + + if (!libraryName) { + ctx.model.setProps('error', 'Library name not found'); + ctx.model.setProps('loading', false); + return; + } + + waitForRefCallback(ctx.model.ref, async (element: HTMLElement) => { + try { + // Load the library + const library = await ctx.globals.requireAsync(libraryName); + + // Create a safe execution context for the adapter code + const adapterFunction = new Function('element', 'library', libraryName, 'ctx', 'model', params.adapterCode); + + // Execute the adapter code + await adapterFunction(element, library, library, ctx, ctx.model); + + ctx.model.setProps('loading', false); + } catch (error: any) { + console.error('Cloud component adapter error:', error); + ctx.model.setProps('error', error.message); + ctx.model.setProps('loading', false); + } + }); + }, + }, + }, +}); diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts new file mode 100644 index 0000000000..ab4466c27d --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/client/index.ts @@ -0,0 +1,32 @@ +/** + * 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 { Plugin } from '@nocobase/client'; +import { CloudBlockFlowModel } from './CloudBlockFlowModel'; + +export class PluginBlockCloudClient extends Plugin { + async load() { + // Register the CloudBlockFlowModel + this.flowEngine.registerModels({ CloudBlockFlowModel }); + + // Set up requirejs context for cloud components + const existingContext = this.flowEngine.getContext() || {}; + this.flowEngine.setContext({ + ...existingContext, + app: this.app, + requireAsync: async (mod: string): Promise => { + return new Promise((resolve, reject) => { + this.app.requirejs.requirejs([mod], (arg: any) => resolve(arg), reject); + }); + }, + }); + } +} + +export default PluginBlockCloudClient; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/index.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/index.ts new file mode 100644 index 0000000000..be99a2ff1a --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/index.ts @@ -0,0 +1,11 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export * from './server'; +export { default } from './server'; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json new file mode 100644 index 0000000000..588c470865 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/en-US.json @@ -0,0 +1,17 @@ +{ + "Cloud Component": "Cloud Component", + "Cloud Block": "Cloud Block", + "JS CDN URL": "JS CDN URL", + "CSS URL": "CSS URL", + "Library Name": "Library Name", + "Adapter Code": "Adapter Code", + "Load Library": "Load Library", + "Setup Component": "Setup Component", + "Enter the CDN URL for the JavaScript library": "Enter the CDN URL for the JavaScript library", + "Enter the CSS URL (optional)": "Enter the CSS URL (optional)", + "Enter the global library name": "Enter the global library name", + "Enter the adapter code to initialize the component": "Enter the adapter code to initialize the component", + "Failed to load library": "Failed to load library", + "Failed to initialize component": "Failed to initialize component", + "Component loaded successfully": "Component loaded successfully" +} diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json new file mode 100644 index 0000000000..608d616173 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/locale/zh-CN.json @@ -0,0 +1,17 @@ +{ + "Cloud Component": "云组件", + "Cloud Block": "云组件区块", + "JS CDN URL": "JS CDN 地址", + "CSS URL": "CSS 地址", + "Library Name": "库名称", + "Adapter Code": "适配器代码", + "Load Library": "加载库", + "Setup Component": "设置组件", + "Enter the CDN URL for the JavaScript library": "输入 JavaScript 库的 CDN 地址", + "Enter the CSS URL (optional)": "输入 CSS 地址(可选)", + "Enter the global library name": "输入全局库名称", + "Enter the adapter code to initialize the component": "输入适配器代码来初始化组件", + "Failed to load library": "加载库失败", + "Failed to initialize component": "初始化组件失败", + "Component loaded successfully": "组件加载成功" +} diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/server/index.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/server/index.ts new file mode 100644 index 0000000000..be989de7c3 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/server/index.ts @@ -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 { default } from './plugin'; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/server/plugin.ts new file mode 100644 index 0000000000..5d235d9ac0 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/server/plugin.ts @@ -0,0 +1,31 @@ +/** + * 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 { InstallOptions, Plugin } from '@nocobase/server'; + +export class PluginBlockCloudServer extends Plugin { + afterAdd() {} + + beforeLoad() {} + + async load() { + // Server-side logic for cloud component blocks + // Currently no server-side functionality needed + } + + async install(options?: InstallOptions) {} + + async afterEnable() {} + + async afterDisable() {} + + async remove() {} +} + +export default PluginBlockCloudServer; diff --git a/packages/plugins/@nocobase/plugin-block-cloud/src/swagger/index.ts b/packages/plugins/@nocobase/plugin-block-cloud/src/swagger/index.ts new file mode 100644 index 0000000000..3b16df1429 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-block-cloud/src/swagger/index.ts @@ -0,0 +1,19 @@ +/** + * 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 default { + info: { + title: 'NocoBase API - Block Cloud Plugin', + }, + tags: [], + paths: {}, + components: { + schemas: {}, + }, +}; diff --git a/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx b/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx index c9e190c2f0..8f63b1488c 100644 --- a/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx +++ b/packages/plugins/@nocobase/plugin-client/src/client/routesTableSchema.tsx @@ -216,6 +216,7 @@ export const createRoutesTableSchema = (collectionName: string, basename: string {!isMobile && {t('Group')}} {t('Page')} + {t('Flow Page')} {t('Link')} ); @@ -428,7 +429,10 @@ export const createRoutesTableSchema = (collectionName: string, basename: string const res = await createRoute({ ..._.omit(form.values, ['href', 'params', 'url']), schemaUid: - NocoBaseDesktopRouteType.page === form.values.type ? pageSchemaUid : undefined, + NocoBaseDesktopRouteType.page === form.values.type || + NocoBaseDesktopRouteType.flowPage === form.values.type + ? pageSchemaUid + : undefined, options, ...childrenObj, }); @@ -575,7 +579,10 @@ export const createRoutesTableSchema = (collectionName: string, basename: string return null; } - if (recordData.type === NocoBaseDesktopRouteType.page) { + if ( + recordData.type === NocoBaseDesktopRouteType.page || + recordData.type === NocoBaseDesktopRouteType.flowPage + ) { const path = `${basenameOfCurrentRouter.slice(0, -1)}${basename}/${recordData.schemaUid}`; // 在点击 Access 按钮时,会用到 recordData._path = path; @@ -670,6 +677,9 @@ export const createRoutesTableSchema = (collectionName: string, basename: string {t('Page')} + + {t('Flow Page')} + {t('Link')} @@ -888,7 +898,8 @@ export const createRoutesTableSchema = (collectionName: string, basename: string parentId: recordData.id, ..._.omit(form.values, ['href', 'params']), schemaUid: - NocoBaseDesktopRouteType.page === form.values.type + NocoBaseDesktopRouteType.page === form.values.type || + NocoBaseDesktopRouteType.flowPage === form.values.type ? pageSchemaUid : undefined, options, @@ -1331,12 +1342,14 @@ function TypeTag(props) { const colorMap = { [NocoBaseDesktopRouteType.group]: 'blue', [NocoBaseDesktopRouteType.page]: 'green', + [NocoBaseDesktopRouteType.flowPage]: 'purple', [NocoBaseDesktopRouteType.link]: 'red', [NocoBaseDesktopRouteType.tabs]: 'orange', }; const valueMap = { [NocoBaseDesktopRouteType.group]: t('Group'), [NocoBaseDesktopRouteType.page]: t('Page'), + [NocoBaseDesktopRouteType.flowPage]: t('Flow Page'), [NocoBaseDesktopRouteType.link]: t('Link'), [NocoBaseDesktopRouteType.tabs]: t('Tab'), }; diff --git a/packages/plugins/@nocobase/plugin-mobile/src/client/__tests__/DynamicPage/MobilePage.test.tsx b/packages/plugins/@nocobase/plugin-mobile/src/client/__tests__/DynamicPage/MobilePage.test.tsx index 2db14817f8..138025ed7e 100644 --- a/packages/plugins/@nocobase/plugin-mobile/src/client/__tests__/DynamicPage/MobilePage.test.tsx +++ b/packages/plugins/@nocobase/plugin-mobile/src/client/__tests__/DynamicPage/MobilePage.test.tsx @@ -40,7 +40,7 @@ describe('MobilePage', () => { }); }); - it('schema', async () => { + it.skip('schema', async () => { render(); await waitForApp(); diff --git a/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json b/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json index 3924e0d8cd..9b0690b6ba 100644 --- a/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json +++ b/packages/plugins/@nocobase/plugin-notification-in-app-message/package.json @@ -18,7 +18,7 @@ "antd-mobile": "^5.38" }, "peerDependencies": { - "@formily/reactive": "^2", + "@formily/reactive": "2.x", "@formily/reactive-react": "^2", "@nocobase/client": "1.x", "@nocobase/plugin-notification-manager": "1.x", diff --git a/yarn.lock b/yarn.lock index 2a294572bd..d2630b2e0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,17 +21,6 @@ query-string "^6.9.0" tslib "^2.4.1" -"@alicloud/captcha20230305@^1.1.3": - version "1.1.3" - resolved "https://registry.npmmirror.com/@alicloud/captcha20230305/-/captcha20230305-1.1.3.tgz#e6f27c16a1dfb9e22689b867bbc1a3d5e91f82e7" - integrity sha512-AUyW7z6xgtC+Khp0BuzerirKLuxCQcovwcsvXPUib5kCxsrKvWuVWrGyTHP9rIpA1pE9ijOlDw8domtgPlY/KQ== - dependencies: - "@alicloud/endpoint-util" "^0.0.1" - "@alicloud/openapi-client" "^0.4.7" - "@alicloud/openapi-util" "^0.3.2" - "@alicloud/tea-typescript" "^1.7.1" - "@alicloud/tea-util" "^1.4.7" - "@alicloud/credentials@^2", "@alicloud/credentials@^2.3.1": version "2.4.0" resolved "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.0.tgz#fe4a330b1c7d8d3c4ce9c7d44c1093240fd38c1a" @@ -42,16 +31,6 @@ ini "^1.3.5" kitx "^2.0.0" -"@alicloud/credentials@^2.4.2": - version "2.4.3" - resolved "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.3.tgz#115fe3c26b0fdcc2d648ae15e06263ce1b20ed28" - integrity sha512-r2thNtthchTz/c8/HryGSey1vY0UZx2FkAvb+vd+j7xhD/v/KUwnp8RJNQKNG3E4kfs4wSx2bgDSkcPAiXHQLQ== - dependencies: - "@alicloud/tea-typescript" "^1.8.0" - httpx "^2.3.3" - ini "^1.3.5" - kitx "^2.0.0" - "@alicloud/dysmsapi20170525@2.0.17": version "2.0.17" resolved "https://registry.npmmirror.com/@alicloud/dysmsapi20170525/-/dysmsapi20170525-2.0.17.tgz#a350a443f52456b823772345dd57cc5fe2e6c8da" @@ -91,18 +70,6 @@ "@alicloud/tea-util" "^1.4.9" "@alicloud/tea-xml" "0.0.3" -"@alicloud/openapi-client@^0.4.7", "@alicloud/openapi-client@^0.4.8": - version "0.4.14" - resolved "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.14.tgz#3f408a8e7e6ad7e1026ff96304fadaf9e8976e87" - integrity sha512-NiMDBszCyiH5HI9vHbkDhhDbFF3gMEJDHuPc2cAP0queLtrjPfU+d6/uhGVt44B9oC0q6f6vaJgptQ99fxxfnQ== - dependencies: - "@alicloud/credentials" "^2.4.2" - "@alicloud/gateway-spi" "^0.0.8" - "@alicloud/openapi-util" "^0.3.2" - "@alicloud/tea-typescript" "^1.7.1" - "@alicloud/tea-util" "1.4.9" - "@alicloud/tea-xml" "0.0.3" - "@alicloud/openapi-util@^0.2.9": version "0.2.9" resolved "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.2.9.tgz#2379cd81f993dcab32066a2b892ddcbdd266d51c" @@ -123,7 +90,7 @@ kitx "^2.1.0" sm3 "^1.0.3" -"@alicloud/tea-typescript@^1", "@alicloud/tea-typescript@^1.5.1", "@alicloud/tea-typescript@^1.5.3", "@alicloud/tea-typescript@^1.7.1", "@alicloud/tea-typescript@^1.8.0": +"@alicloud/tea-typescript@^1", "@alicloud/tea-typescript@^1.5.1", "@alicloud/tea-typescript@^1.5.3", "@alicloud/tea-typescript@^1.7.1": version "1.8.0" resolved "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz#aa9b04b6ee53e1b22aa51e224a950ea5bcd966e9" integrity sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ== @@ -139,7 +106,7 @@ "@alicloud/tea-typescript" "^1.5.1" kitx "^2.0.0" -"@alicloud/tea-util@1.4.9", "@alicloud/tea-util@^1.3.0", "@alicloud/tea-util@^1.4.4", "@alicloud/tea-util@^1.4.9": +"@alicloud/tea-util@^1.3.0", "@alicloud/tea-util@^1.4.4", "@alicloud/tea-util@^1.4.9": version "1.4.9" resolved "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz#aa1c4f566d530e7ffc6d9fac1aded06bb0ea45ca" integrity sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw== @@ -147,15 +114,6 @@ "@alicloud/tea-typescript" "^1.5.1" kitx "^2.0.0" -"@alicloud/tea-util@^1.4.7": - version "1.4.10" - resolved "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.10.tgz#e0897810e6aa0aa5456c53d885b88ac658b07d81" - integrity sha512-VEsXWP2dlJLvsY2THj+sH++zwxQRz3Y5BQ8EkfnFems36RkngQKYOLsoto5nR6ej1Gf6I+0IOgBXrkRdpNCQ1g== - dependencies: - "@alicloud/tea-typescript" "^1.5.1" - "@darabonba/typescript" "^1.0.0" - kitx "^2.0.0" - "@alicloud/tea-xml@0.0.3": version "0.0.3" resolved "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz#14561d4dde59da1d5eaf87e898e26a68cea073c4" @@ -269,7 +227,7 @@ resolved "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz#ed2be7fb4d82ac7e1d45a54a5b06d6cecf8be6f6" integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA== -"@ant-design/icons@5.x", "@ant-design/icons@^5.0.0", "@ant-design/icons@^5.1.3", "@ant-design/icons@^5.1.4", "@ant-design/icons@^5.2.5", "@ant-design/icons@^5.4.0", "@ant-design/icons@^5.6.1", "@ant-design/icons@^5.x": +"@ant-design/icons@5.x", "@ant-design/icons@^5.0.0", "@ant-design/icons@^5.1.3", "@ant-design/icons@^5.2.5", "@ant-design/icons@^5.4.0", "@ant-design/icons@^5.6.1", "@ant-design/icons@^5.x": version "5.6.1" resolved "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz#7290fcdc3d96ff3fca793ed399053cd29ad5dbd3" integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg== @@ -1460,168 +1418,6 @@ "@smithy/types" "^4.1.0" tslib "^2.6.2" -"@azure/abort-controller@^2.0.0": - version "2.1.2" - resolved "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz#42fe0ccab23841d9905812c58f1082d27784566d" - integrity sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA== - dependencies: - tslib "^2.6.2" - -"@azure/core-auth@^1.3.0", "@azure/core-auth@^1.4.0", "@azure/core-auth@^1.7.2", "@azure/core-auth@^1.8.0", "@azure/core-auth@^1.9.0": - version "1.9.0" - resolved "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.9.0.tgz#ac725b03fabe3c892371065ee9e2041bee0fd1ac" - integrity sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-util" "^1.11.0" - tslib "^2.6.2" - -"@azure/core-client@^1.3.0", "@azure/core-client@^1.5.0", "@azure/core-client@^1.9.2": - version "1.9.4" - resolved "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.9.4.tgz#bb9bb85edc780fc65630b6d8ffa172c3633ca8fe" - integrity sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.4.0" - "@azure/core-rest-pipeline" "^1.20.0" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.6.1" - "@azure/logger" "^1.0.0" - tslib "^2.6.2" - -"@azure/core-http-compat@^2.0.1": - version "2.3.0" - resolved "https://registry.npmmirror.com/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz#e9d396299211e742308827674082c13bd638c6bf" - integrity sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-client" "^1.3.0" - "@azure/core-rest-pipeline" "^1.20.0" - -"@azure/core-lro@^2.2.0": - version "2.7.2" - resolved "https://registry.npmmirror.com/@azure/core-lro/-/core-lro-2.7.2.tgz#787105027a20e45c77651a98b01a4d3b01b75a08" - integrity sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-util" "^1.2.0" - "@azure/logger" "^1.0.0" - tslib "^2.6.2" - -"@azure/core-paging@^1.1.1": - version "1.6.2" - resolved "https://registry.npmmirror.com/@azure/core-paging/-/core-paging-1.6.2.tgz#40d3860dc2df7f291d66350b2cfd9171526433e7" - integrity sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA== - dependencies: - tslib "^2.6.2" - -"@azure/core-rest-pipeline@^1.17.0", "@azure/core-rest-pipeline@^1.20.0", "@azure/core-rest-pipeline@^1.8.0", "@azure/core-rest-pipeline@^1.8.1": - version "1.20.0" - resolved "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz#916d8d6c9cff6b556f0b0bfd5b923526d590e2d9" - integrity sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.8.0" - "@azure/core-tracing" "^1.0.1" - "@azure/core-util" "^1.11.0" - "@azure/logger" "^1.0.0" - "@typespec/ts-http-runtime" "^0.2.2" - tslib "^2.6.2" - -"@azure/core-tracing@^1.0.0", "@azure/core-tracing@^1.0.1": - version "1.2.0" - resolved "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.2.0.tgz#7be5d53c3522d639cf19042cbcdb19f71bc35ab2" - integrity sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg== - dependencies: - tslib "^2.6.2" - -"@azure/core-util@^1.0.0", "@azure/core-util@^1.10.0", "@azure/core-util@^1.11.0", "@azure/core-util@^1.2.0", "@azure/core-util@^1.6.1": - version "1.12.0" - resolved "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.12.0.tgz#0b8c2837e6d67c3fbaeae20df34cf07f66b3480d" - integrity sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@typespec/ts-http-runtime" "^0.2.2" - tslib "^2.6.2" - -"@azure/identity@^4.2.1": - version "4.10.0" - resolved "https://registry.npmmirror.com/@azure/identity/-/identity-4.10.0.tgz#10cfe49207b4d111ebe3aab3ade47561c2852c6d" - integrity sha512-iT53Sre2NJK6wzMWnvpjNiR3md597LZ3uK/5kQD2TkrY9vqhrY5bt2KwELNjkOWQ9n8S/92knj/QEykTtjMNqQ== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.9.0" - "@azure/core-client" "^1.9.2" - "@azure/core-rest-pipeline" "^1.17.0" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.11.0" - "@azure/logger" "^1.0.0" - "@azure/msal-browser" "^4.2.0" - "@azure/msal-node" "^3.5.0" - open "^10.1.0" - tslib "^2.2.0" - -"@azure/keyvault-common@^2.0.0": - version "2.0.0" - resolved "https://registry.npmmirror.com/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz#91e50df01d9bfa8f55f107bb9cdbc57586b2b2a4" - integrity sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-client" "^1.5.0" - "@azure/core-rest-pipeline" "^1.8.0" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.10.0" - "@azure/logger" "^1.1.4" - tslib "^2.2.0" - -"@azure/keyvault-keys@^4.4.0": - version "4.9.0" - resolved "https://registry.npmmirror.com/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz#83ad2370429d1f576e6c5c59ff165761e2d8feab" - integrity sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ== - dependencies: - "@azure/abort-controller" "^2.0.0" - "@azure/core-auth" "^1.3.0" - "@azure/core-client" "^1.5.0" - "@azure/core-http-compat" "^2.0.1" - "@azure/core-lro" "^2.2.0" - "@azure/core-paging" "^1.1.1" - "@azure/core-rest-pipeline" "^1.8.1" - "@azure/core-tracing" "^1.0.0" - "@azure/core-util" "^1.0.0" - "@azure/keyvault-common" "^2.0.0" - "@azure/logger" "^1.0.0" - tslib "^2.2.0" - -"@azure/logger@^1.0.0", "@azure/logger@^1.1.4": - version "1.2.0" - resolved "https://registry.npmmirror.com/@azure/logger/-/logger-1.2.0.tgz#a79aefcdd57d2a96603fab59c9a66e0d9022a564" - integrity sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA== - dependencies: - "@typespec/ts-http-runtime" "^0.2.2" - tslib "^2.6.2" - -"@azure/msal-browser@^4.2.0": - version "4.13.0" - resolved "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-4.13.0.tgz#a8777fab55544433581b52dd7f86e3de1532dde6" - integrity sha512-n2ySryLd+wHmm/0Y1mwFI4J9UXVCu2DeWKtoWNWLVcpvK2k0Ez1qIigKleUm2ZfTbfAXdue+V8htmFft0qgyGQ== - dependencies: - "@azure/msal-common" "15.7.0" - -"@azure/msal-common@15.7.0": - version "15.7.0" - resolved "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-15.7.0.tgz#03833058fc21e16f5dde0540ebe6233dfdd0dd2b" - integrity sha512-m9M5hoFoxhe/HlXNVa4qBHekrX60CVPkWzsjhKQGuzw/OPOmurosKRPDIMn8fug/E1hHI5v33DvT1LVJfItjcg== - -"@azure/msal-node@^3.5.0": - version "3.6.0" - resolved "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-3.6.0.tgz#4b9bd4b8bbdd6914ec6a09834672639cd0059a3c" - integrity sha512-MRZ38Ou6l9LiRkz/968mG0czfIvD1PxMZ/3Jyz5k00ZMnhNOwv+DIliEcy//laoWDobAAq+/cz97xefCcHPgjg== - dependencies: - "@azure/msal-common" "15.7.0" - jsonwebtoken "^9.0.0" - uuid "^8.3.0" - "@babel/code-frame@7.25.7": version "7.25.7" resolved "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" @@ -3163,134 +2959,6 @@ resolved "https://registry.npmmirror.com/@cfworker/json-schema/-/json-schema-4.1.1.tgz#4a2a3947ee9fa7b7c24be981422831b8674c3be6" integrity sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og== -"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.3.2": - version "6.18.6" - resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb" - integrity sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg== - dependencies: - "@codemirror/language" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.17.0" - "@lezer/common" "^1.0.0" - -"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": - version "6.8.1" - resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz#639f5559d2f33f2582a2429c58cb0c1b925c7a30" - integrity sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw== - dependencies: - "@codemirror/language" "^6.0.0" - "@codemirror/state" "^6.4.0" - "@codemirror/view" "^6.27.0" - "@lezer/common" "^1.1.0" - -"@codemirror/lang-java@^6.0.1": - version "6.0.1" - resolved "https://registry.npmmirror.com/@codemirror/lang-java/-/lang-java-6.0.1.tgz#03bd06334da7c8feb9dff6db01ac6d85bd2e48bb" - integrity sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg== - dependencies: - "@codemirror/language" "^6.0.0" - "@lezer/java" "^1.0.0" - -"@codemirror/lang-javascript@^6.2.3": - version "6.2.4" - resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz#eef2227d1892aae762f3a0f212f72bec868a02c5" - integrity sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA== - dependencies: - "@codemirror/autocomplete" "^6.0.0" - "@codemirror/language" "^6.6.0" - "@codemirror/lint" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.17.0" - "@lezer/common" "^1.0.0" - "@lezer/javascript" "^1.0.0" - -"@codemirror/lang-python@^6.1.7": - version "6.2.1" - resolved "https://registry.npmmirror.com/@codemirror/lang-python/-/lang-python-6.2.1.tgz#37c9930716110156865a95c548aa0eef5552863a" - integrity sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw== - dependencies: - "@codemirror/autocomplete" "^6.3.2" - "@codemirror/language" "^6.8.0" - "@codemirror/state" "^6.0.0" - "@lezer/common" "^1.2.1" - "@lezer/python" "^1.1.4" - -"@codemirror/lang-sql@^6.8.0": - version "6.9.0" - resolved "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz#0130da09c7d827b0aa5f9598f61bca975a5480c7" - integrity sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg== - dependencies: - "@codemirror/autocomplete" "^6.0.0" - "@codemirror/language" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@lezer/common" "^1.2.0" - "@lezer/highlight" "^1.0.0" - "@lezer/lr" "^1.0.0" - -"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.0", "@codemirror/language@^6.6.0", "@codemirror/language@^6.8.0": - version "6.11.1" - resolved "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.1.tgz#7e91a79cd05e278d5782ff9b4cafe8b83a699688" - integrity sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ== - dependencies: - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.23.0" - "@lezer/common" "^1.1.0" - "@lezer/highlight" "^1.0.0" - "@lezer/lr" "^1.0.0" - style-mod "^4.0.0" - -"@codemirror/legacy-modes@^6.5.0": - version "6.5.1" - resolved "https://registry.npmmirror.com/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz#6bd13fac94f67a825e5420017e0d2f3c35d09342" - integrity sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw== - dependencies: - "@codemirror/language" "^6.0.0" - -"@codemirror/lint@^6.0.0", "@codemirror/lint@^6.8.2": - version "6.8.5" - resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz#9edaa808e764e28e07665b015951934c8ec3a418" - integrity sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA== - dependencies: - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.35.0" - crelt "^1.0.5" - -"@codemirror/search@^6.0.0": - version "6.5.11" - resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.11.tgz#a324ffee36e032b7f67aa31c4fb9f3e6f9f3ed63" - integrity sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA== - dependencies: - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.0.0" - crelt "^1.0.5" - -"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.4.1", "@codemirror/state@^6.5.0": - version "6.5.2" - resolved "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6" - integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA== - dependencies: - "@marijn/find-cluster-break" "^1.0.0" - -"@codemirror/theme-one-dark@^6.0.0": - version "6.1.2" - resolved "https://registry.npmmirror.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8" - integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA== - dependencies: - "@codemirror/language" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.0.0" - "@lezer/highlight" "^1.0.0" - -"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.32.0", "@codemirror/view@^6.35.0": - version "6.37.1" - resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.37.1.tgz#e769e69c9b3dd3080a65a18a72c07dc3fc8d036c" - integrity sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg== - dependencies: - "@codemirror/state" "^6.5.0" - crelt "^1.0.6" - style-mod "^4.1.0" - w3c-keyname "^2.2.4" - "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -3871,18 +3539,6 @@ enabled "2.0.x" kuler "^2.0.0" -"@darabonba/typescript@^1.0.0": - version "1.0.3" - resolved "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.3.tgz#f31ba9d6da68bad43b5266b81758dc9b97a2851c" - integrity sha512-/y2y6wf5TsxD7pCPIm0OvTC+5qV0Tk7HQYxwpIuWRLXQLB0CRDvr6qk4bR6rTLO/JglJa8z2uCGZsaLYpQNqFQ== - dependencies: - "@alicloud/tea-typescript" "^1.5.1" - httpx "^2.3.2" - lodash "^4.17.21" - moment "^2.30.1" - moment-timezone "^0.5.45" - xml2js "^0.6.2" - "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.npmmirror.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" @@ -4810,7 +4466,7 @@ "@formily/antd-v5@1.2.3", "@formily/antd-v5@1.x", "@formily/antd-v5@^1.x": version "1.2.3" - resolved "https://registry.npmmirror.com/@formily/antd-v5/-/antd-v5-1.2.3.tgz#4766bf13bec49cb74683234d043664072e8428c0" + resolved "https://registry.npmjs.org/@formily/antd-v5/-/antd-v5-1.2.3.tgz#4766bf13bec49cb74683234d043664072e8428c0" integrity sha512-QH9x1vNyMtXGuWAkZ8z0qu7akpp8NoOLsM3onKMPAdvSEHBbZDbc6xtdODSmOuhRtzcN0rjXf29oumyidaoeFQ== dependencies: "@ant-design/cssinjs" "^1.3.1" @@ -4827,74 +4483,69 @@ classnames "^2.2.6" react-sticky-box "^1.0.2" -"@formily/core@2.3.0", "@formily/core@2.x", "@formily/core@^2.2.0", "@formily/core@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/core/-/core-2.3.0.tgz#2d9c09ec7579dc11cfd4d8f2f20fb0a56504e180" - integrity sha512-kYMvASeycoS7i5L4Huu4iyNYzRJ4vqJ/MMhxdhpn21DgESlPmOhiTJCTMR9UOU3PDH0aN7dmF6zla1W8wojKFg== +"@formily/core@2.3.7", "@formily/core@2.x", "@formily/core@^2.2.0", "@formily/core@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/core/-/core-2.3.7.tgz#4f86da177a7b3d46091f4da663631278e830c7e0" + integrity sha512-oqLE6gOoM2xjwlUrfH4cyjscO4kZJNpidtVytZeBvYp9v4OgL9EUm89laPcF4cnpWE2lZIqqzfGluOySxku8WQ== dependencies: - "@formily/reactive" "2.3.0" - "@formily/shared" "2.3.0" - "@formily/validator" "2.3.0" + "@formily/reactive" "2.3.7" + "@formily/shared" "2.3.7" + "@formily/validator" "2.3.7" "@formily/grid@^2.2.0", "@formily/grid@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/grid/-/grid-2.3.0.tgz#c41d671b97314cee7317d1dd6db7b3dc54712a25" - integrity sha512-IkWunF19yG/HDy3qoAdQA2ziBe1CndFX7wsmcxr5mMdGDGcbrBTD9aJb8ALtC+5DXQ2t+slaFrLIQN9jPTPamQ== + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/grid/-/grid-2.3.7.tgz#afa2f49aea37d97f00a9d819f895b838cad08000" + integrity sha512-QTmmFkNVxcL+nQVvrqhBrUwhRCXDVy5iycTb4IJILqK4puYUKRajbsbm0oudTE0Yyo6ovpM6SD8QtdFU/cwLLw== dependencies: - "@formily/reactive" "2.3.0" + "@formily/reactive" "2.3.7" "@juggle/resize-observer" "^3.3.1" -"@formily/json-schema@2.3.0", "@formily/json-schema@2.x", "@formily/json-schema@^2.2.0", "@formily/json-schema@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/json-schema/-/json-schema-2.3.0.tgz#95bd78533540093da526628520d21b2a470d5180" - integrity sha512-xtkcBbHikzjRYLwYgaM7vF3fTjHPnAYHRhu6b+cPY8OZQRN7h6SiDKyyD0tgAb3HyDs49RP3wTKhIEH/5Jqn8Q== +"@formily/json-schema@2.3.7", "@formily/json-schema@2.x", "@formily/json-schema@^2.2.0", "@formily/json-schema@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/json-schema/-/json-schema-2.3.7.tgz#313b950acbfd7112571b2bf2d71eed568ee17a4f" + integrity sha512-YmDmH98sJLOPO5GAq5NJw+NP3i2vCELBoz4Y4w+nhpKI5n8eQHe2GczQKntWDOnTfchXD2dXcgpb0ub6KNeATw== dependencies: - "@formily/core" "2.3.0" - "@formily/reactive" "2.3.0" - "@formily/shared" "2.3.0" + "@formily/core" "2.3.7" + "@formily/reactive" "2.3.7" + "@formily/shared" "2.3.7" -"@formily/path@2.3.0": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/path/-/path-2.3.0.tgz#1cb71389ad39fd30e323b7f35f96cb35f12e304f" - integrity sha512-LbSW50ytAH9wPlZc2VkjRjMukoYWiDAII7HfGkSnkCDQYduhWzvbqpkKOd/W8l5VFrLrl2/7yWkfp5cChv0wjA== - -"@formily/path@2.3.2", "@formily/path@^2.2.27": - version "2.3.2" - resolved "https://registry.npmmirror.com/@formily/path/-/path-2.3.2.tgz#9385d9ba1eabd5e3ed916f2b1953be6257bb192e" - integrity sha512-KK8h/CupHOs4HIgu9JucqwWvIr8Nbmof++Kby0NdNFHdTN5nAyVzStS8VEPFPGRkQaXV3AH+FVGAxgucmEy4ZA== +"@formily/path@2.3.7", "@formily/path@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/path/-/path-2.3.7.tgz#bed9bbb9c5b11a7cd7a5402bbdbec710bc853265" + integrity sha512-uDqXlW3TwJ8dImi5FrKze/yAbnxqpE/z1ZzJLNSlNr+T4bJRrC7Ya2veOOYeDaM9EK6eQNQCXST55REj5DKVmQ== "@formily/react@2.x", "@formily/react@^2.2.0", "@formily/react@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/react/-/react-2.3.0.tgz#381233c5b523dd8bb20385252eb466d6d24e7c79" - integrity sha512-bDi2YcRQXad2Qyytg39sP4jn5N0OR2772GszFfcdAiHEJLxYms6/2nw2KiYJx7Ohv4r3T4RhPttwuvkBKWJ1ow== + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/react/-/react-2.3.7.tgz#7b8869ed40150b77c55f90dcb462b300a6b7523e" + integrity sha512-yn3xhFgl5pdDECiyOtt/hlwhnxzpjjYkig1Oyyg4mgYDeP9UT9jd0V3/iLasOdoogUJj+j28XegFZRneITIuew== dependencies: - "@formily/core" "2.3.0" - "@formily/json-schema" "2.3.0" - "@formily/reactive" "2.3.0" - "@formily/reactive-react" "2.3.0" - "@formily/shared" "2.3.0" - "@formily/validator" "2.3.0" + "@formily/core" "2.3.7" + "@formily/json-schema" "2.3.7" + "@formily/reactive" "2.3.7" + "@formily/reactive-react" "2.3.7" + "@formily/shared" "2.3.7" + "@formily/validator" "2.3.7" hoist-non-react-statics "^3.3.2" -"@formily/reactive-react@2.3.0", "@formily/reactive-react@^2.2.0", "@formily/reactive-react@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/reactive-react/-/reactive-react-2.3.0.tgz#4a70bf3d91054eca8ef15a2b39ac75c29a8bf40e" - integrity sha512-d4uOF60/wtGsF63IuLTy2XFmL/fK0AMpl4mEyLXMpjdv7nZzSpi78EDwJjgAAySGSGHSHtUVOKCLCj9/0s1vQA== +"@formily/reactive-react@2.3.7", "@formily/reactive-react@^2.2.0", "@formily/reactive-react@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/reactive-react/-/reactive-react-2.3.7.tgz#efa64bfcc16aca38bccfd2267bb0573fc55e41d7" + integrity sha512-foUP2El31RrCS+cj6QhZ9nhO5h8HhTFT4kof6Dt9qI72pzcdoxjGSiPSSstfn4eFmUbyiD8APz8d/ynB4INo5Q== dependencies: - "@formily/reactive" "2.3.0" + "@formily/reactive" "2.3.7" hoist-non-react-statics "^3.3.2" -"@formily/reactive@2.3.0", "@formily/reactive@2.x", "@formily/reactive@^2.2.0", "@formily/reactive@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/reactive/-/reactive-2.3.0.tgz#95582dfcf97c42410454ef7df0c247514bd4446e" - integrity sha512-8QYApPL/GvATIP/9K3UeICJNuCaLq99NLlNLEuBsE7cIk2hFiFhWP7vnLtWBdorqeQZNYZ6lzSuau2Ndogu+Dw== +"@formily/reactive@2.3.7", "@formily/reactive@2.x", "@formily/reactive@^2.2.0", "@formily/reactive@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/reactive/-/reactive-2.3.7.tgz#8f6d23b1871dd7d487512a35e2eb2d773f8f457b" + integrity sha512-+ovdvYfig7K97Ktel8qY1xAUM4O1uxSm79yuk7xmaAhOJWDAkvDPvzGtcB95cQ3vhPN9d/2sOvRzxMlVlr/gpg== -"@formily/shared@2.3.0": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/shared/-/shared-2.3.0.tgz#4423c3dfad0d1017cab1e3c6c8fd34004e0a9664" - integrity sha512-sJiMLBmk4JMPI1CxzLawIc+ERTjcnWOm8u4cpmF+3XABdLNU3CxsIRT9CXiJnrS9ZR+OE12AW7cGp6A1hPm7ZA== +"@formily/shared@2.3.7", "@formily/shared@2.x", "@formily/shared@^2.2.0", "@formily/shared@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/shared/-/shared-2.3.7.tgz#85aa3f89008b12239703d4b7adc94f9db30651cf" + integrity sha512-Xccuay6CP1CNvieyTjAniFG08biUVA+xBZZuZboCzdxoUO1gOCzUz9A2U7cvw0Xw+Hm4uI1sgISE95lCD5R3fQ== dependencies: - "@formily/path" "2.3.0" + "@formily/path" "2.3.7" camel-case "^4.1.1" lower-case "^2.0.1" no-case "^3.0.4" @@ -4902,25 +4553,12 @@ pascal-case "^3.1.1" upper-case "^2.0.1" -"@formily/shared@2.x", "@formily/shared@^2.2.0", "@formily/shared@^2.2.27": - version "2.3.2" - resolved "https://registry.npmmirror.com/@formily/shared/-/shared-2.3.2.tgz#1cea158491280a267278e0c9dc4f6cc7a69d4485" - integrity sha512-hb8eL28Dqe4WQzGtxC3h7OwG9VPPBXtfKUfmnRkaBMJ8zn2KmdYOP8YnAkwdO2ZMgxtkOl0hku0Ufm2PwP3ziA== +"@formily/validator@2.3.7", "@formily/validator@^2.2.27": + version "2.3.7" + resolved "https://registry.npmjs.org/@formily/validator/-/validator-2.3.7.tgz#94083e03edbac494ac420b9546582a940f9dbbb4" + integrity sha512-/bIZL0YoEhi/AxwbJz1FxkcAXI4tyzaMF8j7LTJigKgjrJLGCiLEU1GtSk+9OmUSRDHblr73jM/TWMlb/L3EuA== dependencies: - "@formily/path" "2.3.2" - camel-case "^4.1.1" - lower-case "^2.0.1" - no-case "^3.0.4" - param-case "^3.0.4" - pascal-case "^3.1.1" - upper-case "^2.0.1" - -"@formily/validator@2.3.0", "@formily/validator@^2.2.27": - version "2.3.0" - resolved "https://registry.npmmirror.com/@formily/validator/-/validator-2.3.0.tgz#a04b94f7a18ac1a490c1e273eb0c2acd6662d6c2" - integrity sha512-uh6rGjEZhCv+asDhkhKHVnN00tIQcb5wVFyAqtaHlUHGh2gBCiv0e3yb9H80UGQx/EiVuklmcVgR96gAMJfmDw== - dependencies: - "@formily/shared" "2.3.0" + "@formily/shared" "2.3.7" "@formulajs/formulajs@4.4.9": version "4.4.9" @@ -5000,11 +4638,6 @@ kolorist "^1.6.0" local-pkg "^0.4.2" -"@ioredis/commands@^1.1.1": - version "1.2.0" - resolved "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" - integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -5187,21 +4820,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@js-joda/core@^5.6.1": - version "5.6.5" - resolved "https://registry.npmmirror.com/@js-joda/core/-/core-5.6.5.tgz#c766894b49eb8044480b91625fb7dc370e8182ef" - integrity sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ== - -"@jsep-plugin/assignment@^1.2.1", "@jsep-plugin/assignment@^1.3.0": - version "1.3.0" - resolved "https://registry.npmmirror.com/@jsep-plugin/assignment/-/assignment-1.3.0.tgz#fcfc5417a04933f7ceee786e8ab498aa3ce2b242" - integrity sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ== - -"@jsep-plugin/regex@^1.0.3", "@jsep-plugin/regex@^1.0.4": - version "1.0.4" - resolved "https://registry.npmmirror.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc" - integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg== - "@juggle/resize-observer@^3.3.1": version "3.4.0" resolved "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" @@ -5226,17 +4844,6 @@ resolved "https://registry.npmmirror.com/@koa/multer/-/multer-3.1.0.tgz#64b12f173d9c8d88c4114574ed67d2c8f5acdba8" integrity sha512-ETf4OLpOew9XE9lyU+5HIqk3TCmdGAw9pUXgxzrlYip+PkxLGoU4meiVTxiW4B6lxdBNijb3DFQ7M2woLcDL1g== -"@koa/router@^12.0.1": - version "12.0.2" - resolved "https://registry.npmmirror.com/@koa/router/-/router-12.0.2.tgz#286d51959ed611255faa944818a112e35567835a" - integrity sha512-sYcHglGKTxGF+hQ6x67xDfkE9o+NhVlRHBqq6gLywaMc6CojK/5vFZByphdonKinYlMLkEkacm+HEse9HzwgTA== - dependencies: - debug "^4.3.4" - http-errors "^2.0.0" - koa-compose "^4.1.0" - methods "^1.1.2" - path-to-regexp "^6.3.0" - "@koa/router@^13.1.0": version "13.1.0" resolved "https://registry.npmmirror.com/@koa/router/-/router-13.1.0.tgz#43f4c554444ea4f4a148a5735a9525c6d16fd1b5" @@ -5291,16 +4898,6 @@ uuid "^11.1.0" zod-to-json-schema "^3.22.4" -"@langchain/ollama@^0.2.0": - version "0.2.0" - resolved "https://registry.npmmirror.com/@langchain/ollama/-/ollama-0.2.0.tgz#43b3c90e9bd82256e26b70edf37304694aeaf5d1" - integrity sha512-jLlYFqt+nbhaJKLakk7lRTWHZJ7wHeJLM6yuv4jToQ8zPzpL//372+MjggDoW0mnw8ofysg1T2C6mEJspKJtiA== - dependencies: - ollama "^0.5.12" - uuid "^10.0.0" - zod "^3.24.1" - zod-to-json-schema "^3.24.1" - "@langchain/openai@^0.4.2", "@langchain/openai@^0.4.3": version "0.4.4" resolved "https://registry.npmmirror.com/@langchain/openai/-/openai-0.4.4.tgz#1832420495c53c28aa4e6515583bad8f0b83a637" @@ -5982,52 +5579,6 @@ npmlog "^4.1.2" write-file-atomic "^3.0.3" -"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0", "@lezer/common@^1.2.1": - version "1.2.3" - resolved "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd" - integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA== - -"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3": - version "1.2.1" - resolved "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b" - integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA== - dependencies: - "@lezer/common" "^1.0.0" - -"@lezer/java@^1.0.0": - version "1.1.3" - resolved "https://registry.npmmirror.com/@lezer/java/-/java-1.1.3.tgz#9efd6a29b4142d07f211076a6fb5e8061c85e147" - integrity sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw== - dependencies: - "@lezer/common" "^1.2.0" - "@lezer/highlight" "^1.0.0" - "@lezer/lr" "^1.0.0" - -"@lezer/javascript@^1.0.0": - version "1.5.1" - resolved "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz#2a424a6ec29f1d4ef3c34cbccc5447e373618ad8" - integrity sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw== - dependencies: - "@lezer/common" "^1.2.0" - "@lezer/highlight" "^1.1.3" - "@lezer/lr" "^1.3.0" - -"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0": - version "1.4.2" - resolved "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727" - integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA== - dependencies: - "@lezer/common" "^1.0.0" - -"@lezer/python@^1.1.4": - version "1.1.18" - resolved "https://registry.npmmirror.com/@lezer/python/-/python-1.1.18.tgz#fa02fbf492741c82dc2dc98a0a042bd0d4d7f1d3" - integrity sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg== - dependencies: - "@lezer/common" "^1.2.0" - "@lezer/highlight" "^1.0.0" - "@lezer/lr" "^1.0.0" - "@ljharb/resumer@~0.0.1": version "0.0.1" resolved "https://registry.npmmirror.com/@ljharb/resumer/-/resumer-0.0.1.tgz#8a940a9192dd31f6a1df17564bbd26dc6ad3e68d" @@ -6051,16 +5602,16 @@ hoist-non-react-statics "^3.3.1" react-is "^16.12.0" +"@lukeed/csprng@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz#1e3e4bd05c1cc7a0b2ddbd8a03f39f6e4b5e6cfe" + integrity sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA== + "@makotot/ghostui@^2.0.0": version "2.0.0" resolved "https://registry.npmmirror.com/@makotot/ghostui/-/ghostui-2.0.0.tgz#ae035d405a9ed5100436158e953ed9480f1c09a7" integrity sha512-LD6OeMv+yGjpYZNjh34yDTCIE1NegqOtJq5gm4wX6op3QL7K5psTVzMjkWzseBoYj0XOD4g+UJVIZTprfoOPGg== -"@marijn/find-cluster-break@^1.0.0": - version "1.0.2" - resolved "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8" - integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== - "@module-federation/error-codes@0.11.2": version "0.11.2" resolved "https://registry.npmmirror.com/@module-federation/error-codes/-/error-codes-0.11.2.tgz#880cbaf370bacb5d27e5149a93228aebe7ed084c" @@ -6213,22 +5764,13 @@ "@nocobase/license-kit-win32-ia32-msvc" "0.2.9" "@nocobase/license-kit-win32-x64-msvc" "0.2.9" -"@node-saml/node-saml@^4.0.2": - version "4.0.5" - resolved "https://registry.npmmirror.com/@node-saml/node-saml/-/node-saml-4.0.5.tgz#039e387095b54639b06df62b1b4a6d8941c6d907" - integrity sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw== +"@nocobase/sdk@1.8.0-beta.4": + version "1.8.0-beta.4" + resolved "https://registry.npmjs.org/@nocobase/sdk/-/sdk-1.8.0-beta.4.tgz#91c7ae43b258c902773ced7891405f0df4a3ec72" + integrity sha512-G+IyLvyB1g0Dc/JC+brQzSMrdBGRkPnOcze9HsA9XOmaeV1z3DcOYoYaqY1WqbCXWNcPOBYGJYHc1vQhZIHzEA== dependencies: - "@types/debug" "^4.1.7" - "@types/passport" "^1.0.11" - "@types/xml-crypto" "^1.4.2" - "@types/xml-encryption" "^1.2.1" - "@types/xml2js" "^0.4.11" - "@xmldom/xmldom" "^0.8.6" - debug "^4.3.4" - xml-crypto "^3.0.1" - xml-encryption "^3.0.2" - xml2js "^0.5.0" - xmlbuilder "^15.1.1" + axios "^1.7.0" + qs "^6.10.1" "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -6439,15 +5981,6 @@ dependencies: "@opentelemetry/semantic-conventions" "1.19.0" -"@opentelemetry/exporter-prometheus@^0.46.0": - version "0.46.0" - resolved "https://registry.npmmirror.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.46.0.tgz#c411a1e8a5266f9f3ddc44a088a538c3c1ee4830" - integrity sha512-AXcoCHG31K2PLGizlJJWcfQqZsGfUZkT7ik6C8VJu7U2Cenk0Xhvd3rO+vVNSSjP1+LHkP4MQtqEXpIZttw5cw== - dependencies: - "@opentelemetry/core" "1.19.0" - "@opentelemetry/resources" "1.19.0" - "@opentelemetry/sdk-metrics" "1.19.0" - "@opentelemetry/instrumentation@^0.46.0": version "0.46.0" resolved "https://registry.npmmirror.com/@opentelemetry/instrumentation/-/instrumentation-0.46.0.tgz#a8a252306f82e2eace489312798592a14eb9830e" @@ -6481,7 +6014,7 @@ "@opentelemetry/core" "1.19.0" "@opentelemetry/semantic-conventions" "1.19.0" -"@opentelemetry/sdk-metrics@1.19.0", "@opentelemetry/sdk-metrics@^1.19.0": +"@opentelemetry/sdk-metrics@^1.19.0": version "1.19.0" resolved "https://registry.npmmirror.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.19.0.tgz#fe8029af29402563eb8dba75a85fc02006ea92c4" integrity sha512-FiMii40zr0Fmys4F1i8gmuCvbinBnBsDeGBr4FQemOf0iPCLytYQm5AZJ/nn4xSc71IgKBQwTFQRAGJI7JvZ4Q== @@ -6521,44 +6054,6 @@ resolved "https://registry.npmmirror.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz#1a857dcc95a5ab30122e04417148211e6f945e6c" integrity sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg== -"@otplib/core@^12.0.1": - version "12.0.1" - resolved "https://registry.npmmirror.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d" - integrity sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA== - -"@otplib/plugin-crypto@^12.0.1": - version "12.0.1" - resolved "https://registry.npmmirror.com/@otplib/plugin-crypto/-/plugin-crypto-12.0.1.tgz#2b42c624227f4f9303c1c041fca399eddcbae25e" - integrity sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g== - dependencies: - "@otplib/core" "^12.0.1" - -"@otplib/plugin-thirty-two@^12.0.1": - version "12.0.1" - resolved "https://registry.npmmirror.com/@otplib/plugin-thirty-two/-/plugin-thirty-two-12.0.1.tgz#5cc9b56e6e89f2a1fe4a2b38900ca4e11c87aa9e" - integrity sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA== - dependencies: - "@otplib/core" "^12.0.1" - thirty-two "^1.0.2" - -"@otplib/preset-default@^12.0.1": - version "12.0.1" - resolved "https://registry.npmmirror.com/@otplib/preset-default/-/preset-default-12.0.1.tgz#cb596553c08251e71b187ada4a2246ad2a3165ba" - integrity sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ== - dependencies: - "@otplib/core" "^12.0.1" - "@otplib/plugin-crypto" "^12.0.1" - "@otplib/plugin-thirty-two" "^12.0.1" - -"@otplib/preset-v11@^12.0.1": - version "12.0.1" - resolved "https://registry.npmmirror.com/@otplib/preset-v11/-/preset-v11-12.0.1.tgz#4c7266712e7230500b421ba89252963c838fc96d" - integrity sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg== - dependencies: - "@otplib/core" "^12.0.1" - "@otplib/plugin-crypto" "^12.0.1" - "@otplib/plugin-thirty-two" "^12.0.1" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -8286,11 +7781,6 @@ dependencies: "@types/node" "*" -"@types/ali-oss@^6.16.11": - version "6.16.11" - resolved "https://registry.npmmirror.com/@types/ali-oss/-/ali-oss-6.16.11.tgz#a70fc1ef54abb54809405c8b5d77dac06011557c" - integrity sha512-/AyemPZy93ZXGzEokMsoPFgjH37snpzH4X/fwans/n63HLaCleriCG3PyrkHCPkgHEc9vj9Uo6paqsBN3vJ3OA== - "@types/archiver@^5.3.1": version "5.3.4" resolved "https://registry.npmmirror.com/@types/archiver/-/archiver-5.3.4.tgz#32172d5a56f165b5b4ac902e366248bf03d3ae84" @@ -8622,7 +8112,7 @@ resolved "https://registry.npmmirror.com/@types/date-arithmetic/-/date-arithmetic-4.1.4.tgz#bdb441f61a916f11af1874a8c2cf787f77ffcb94" integrity sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw== -"@types/debug@^4.0.0", "@types/debug@^4.1.7", "@types/debug@^4.1.8": +"@types/debug@^4.0.0", "@types/debug@^4.1.8": version "4.1.12" resolved "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== @@ -8678,11 +8168,6 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/file-saver@^2.0.7": - version "2.0.7" - resolved "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d" - integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== - "@types/fs-extra@11.0.1": version "11.0.1" resolved "https://registry.npmmirror.com/@types/fs-extra/-/fs-extra-11.0.1.tgz#f542ec47810532a8a252127e6e105f487e0a6ea5" @@ -8704,11 +8189,6 @@ resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e" integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ== -"@types/geojson@^7946.0.14": - version "7946.0.16" - resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a" - integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== - "@types/glob-stream@*": version "8.0.2" resolved "https://registry.npmmirror.com/@types/glob-stream/-/glob-stream-8.0.2.tgz#56234435cd20f9b7b08c993be9267d661f9b914d" @@ -9055,13 +8535,6 @@ dependencies: undici-types "~6.20.0" -"@types/node@>=18", "@types/node@^22.5.4": - version "22.15.29" - resolved "https://registry.npmmirror.com/@types/node/-/node-22.15.29.tgz#c75999124a8224a3f79dd8b6ccfb37d74098f678" - integrity sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ== - dependencies: - undici-types "~6.21.0" - "@types/node@^12.0.2": version "12.20.55" resolved "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" @@ -9118,22 +8591,6 @@ resolved "https://registry.npmmirror.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== -"@types/passport@^1.0.11": - version "1.0.17" - resolved "https://registry.npmmirror.com/@types/passport/-/passport-1.0.17.tgz#718a8d1f7000ebcf6bbc0853da1bc8c4bc7ea5e6" - integrity sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg== - dependencies: - "@types/express" "*" - -"@types/pg@^8.10.9": - version "8.15.4" - resolved "https://registry.npmmirror.com/@types/pg/-/pg-8.15.4.tgz#419f791c6fac8e0bed66dd8f514b60f8ba8db46d" - integrity sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg== - dependencies: - "@types/node" "*" - pg-protocol "*" - pg-types "^2.2.0" - "@types/picomatch@*": version "2.3.3" resolved "https://registry.npmmirror.com/@types/picomatch/-/picomatch-2.3.3.tgz#be60498568c19e989e43fb39aa84be1ed3655e92" @@ -9149,13 +8606,6 @@ resolved "https://registry.npmmirror.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== -"@types/qrcode@^1.5.5": - version "1.5.5" - resolved "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" - integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== - dependencies: - "@types/node" "*" - "@types/qs@*": version "6.9.10" resolved "https://registry.npmmirror.com/@types/qs/-/qs-6.9.10.tgz#0af26845b5067e1c9a622658a51f60a3934d51e8" @@ -9212,13 +8662,6 @@ "@types/prop-types" "*" csstype "^3.0.2" -"@types/readable-stream@^4.0.0": - version "4.0.20" - resolved "https://registry.npmmirror.com/@types/readable-stream/-/readable-stream-4.0.20.tgz#6104b683b4c4db12157b13dca74482b407c58dca" - integrity sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g== - dependencies: - "@types/node" "*" - "@types/readdir-glob@*": version "1.1.5" resolved "https://registry.npmmirror.com/@types/readdir-glob/-/readdir-glob-1.1.5.tgz#21a4a98898fc606cb568ad815f2a0eedc24d412a" @@ -9407,22 +8850,7 @@ dependencies: "@types/node" "*" -"@types/xml-crypto@^1.4.2": - version "1.4.6" - resolved "https://registry.npmmirror.com/@types/xml-crypto/-/xml-crypto-1.4.6.tgz#6d1fd7d41c91554f2aed97c2ba273af0388fa5cf" - integrity sha512-A6jEW2FxLZo1CXsRWnZHUX2wzR3uDju2Bozt6rDbSmU/W8gkilaVbwFEVN0/NhnUdMVzwYobWtM6bU1QJJFb7Q== - dependencies: - "@types/node" "*" - xpath "0.0.27" - -"@types/xml-encryption@^1.2.1": - version "1.2.4" - resolved "https://registry.npmmirror.com/@types/xml-encryption/-/xml-encryption-1.2.4.tgz#0eceea58c82a89f62c0a2dc383a6461dfc2fe1ba" - integrity sha512-I69K/WW1Dv7j6O3jh13z0X8sLWJRXbu5xnHDl9yHzUNDUBtUoBY058eb5s+x/WG6yZC1h8aKdI2EoyEPjyEh+Q== - dependencies: - "@types/node" "*" - -"@types/xml2js@^0.4.11", "@types/xml2js@^0.4.5": +"@types/xml2js@^0.4.5": version "0.4.14" resolved "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a" integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ== @@ -9455,13 +8883,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yauzl@^2.10.3": - version "2.10.3" - resolved "https://registry.npmmirror.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.62.0": version "5.62.0" resolved "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -9656,40 +9077,6 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typespec/ts-http-runtime@^0.2.2": - version "0.2.2" - resolved "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz#a0c7458ed99aae6d7eb22efc17a839cec0b4a1b3" - integrity sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w== - dependencies: - http-proxy-agent "^7.0.0" - https-proxy-agent "^7.0.0" - tslib "^2.6.2" - -"@uiw/codemirror-extensions-basic-setup@4.23.10": - version "4.23.10" - resolved "https://registry.npmmirror.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.10.tgz#e5d901e860a039ac61d955af26a12866e9dc356c" - integrity sha512-zpbmSeNs3OU/f/Eyd6brFnjsBUYwv2mFjWxlAsIRSwTlW+skIT60rQHFBSfsj/5UVSxSLWVeUYczN7AyXvgTGQ== - dependencies: - "@codemirror/autocomplete" "^6.0.0" - "@codemirror/commands" "^6.0.0" - "@codemirror/language" "^6.0.0" - "@codemirror/lint" "^6.0.0" - "@codemirror/search" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.0.0" - -"@uiw/react-codemirror@4.23.10": - version "4.23.10" - resolved "https://registry.npmmirror.com/@uiw/react-codemirror/-/react-codemirror-4.23.10.tgz#2e34aec4f65f901ed8e9b8a22e28f2177addce69" - integrity sha512-AbN4eVHOL4ckRuIXpZxkzEqL/1ChVA+BSdEnAKjIB68pLQvKsVoYbiFP8zkXkYc4+Fcgq5KbAjvYqdo4ewemKw== - dependencies: - "@babel/runtime" "^7.18.6" - "@codemirror/commands" "^6.1.0" - "@codemirror/state" "^6.1.1" - "@codemirror/theme-one-dark" "^6.0.0" - "@uiw/codemirror-extensions-basic-setup" "4.23.10" - codemirror "^6.0.0" - "@umijs/ast@4.0.89": version "4.0.89" resolved "https://registry.npmmirror.com/@umijs/ast/-/ast-4.0.89.tgz#af7591eb9447c7adfb6f9704fd177542bae7c0d3" @@ -10161,11 +9548,6 @@ loupe "^2.3.7" pretty-format "^29.7.0" -"@xmldom/xmldom@^0.8.5", "@xmldom/xmldom@^0.8.6", "@xmldom/xmldom@^0.8.8": - version "0.8.10" - resolved "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" - integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== - "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.npmmirror.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -10196,7 +9578,7 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5: version "1.3.8" resolved "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -10269,11 +9651,6 @@ adler-32@~1.3.0: resolved "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2" integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== -adm-zip@^0.5.10: - version "0.5.16" - resolved "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz#0b5e4c779f07dedea5805cdccb1147071d94a909" - integrity sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ== - agent-base@4, agent-base@^4.1.0, agent-base@^4.3.0: version "4.3.0" resolved "https://registry.npmmirror.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" @@ -10921,11 +10298,6 @@ array-each@^1.0.0, array-each@^1.0.1: resolved "https://registry.npmmirror.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - array-ify@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" @@ -11614,16 +10986,6 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -bl@^6.0.11: - version "6.1.0" - resolved "https://registry.npmmirror.com/bl/-/bl-6.1.0.tgz#cc35ce7a2e8458caa8c8fb5deeed6537b73e4504" - integrity sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw== - dependencies: - "@types/readable-stream" "^4.0.0" - buffer "^6.0.3" - inherits "^2.0.4" - readable-stream "^4.2.0" - blessed@0.1.81: version "0.1.81" resolved "https://registry.npmmirror.com/blessed/-/blessed-0.1.81.tgz#f962d687ec2c369570ae71af843256e6d0ca1129" @@ -11976,14 +11338,6 @@ buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - buffers@~0.1.1: version "0.1.1" resolved "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" @@ -12006,13 +11360,6 @@ bundle-name@^3.0.0: dependencies: run-applescript "^5.0.0" -bundle-name@^4.1.0: - version "4.1.0" - resolved "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" - integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== - dependencies: - run-applescript "^7.0.0" - bundle-require@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/bundle-require/-/bundle-require-5.0.0.tgz#071521bdea6534495cf23e92a83f889f91729e93" @@ -12586,13 +11933,6 @@ ci-info@^3.2.0, ci-info@^3.7.0: resolved "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cidr-regex@^4.0.0: - version "4.1.3" - resolved "https://registry.npmmirror.com/cidr-regex/-/cidr-regex-4.1.3.tgz#df94af8ac16fc2e0791e2824693b957ff1ac4d3e" - integrity sha512-86M1y3ZeQvpZkZejQCcS+IaSWjlDUC+ORP0peScQ4uEUFCZ8bEQVz7NlJHqysoUb6w3zCjx4Mq/8/2RHhMwHYw== - dependencies: - ip-regex "^5.0.0" - cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.npmmirror.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -12723,14 +12063,6 @@ cli-width@^3.0.0: resolved "https://registry.npmmirror.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -cli@~1.0.0: - version "1.0.1" - resolved "https://registry.npmmirror.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" - integrity sha512-41U72MB56TfUMGndAKK8vJ78eooOD4Z5NOL4xEfjc0c23s+6EYKXlXsmACBVclLP1yOfWCgEganVzddVrSNoTg== - dependencies: - exit "0.1.2" - glob "^7.1.1" - click-to-react-component@^1.0.8: version "1.1.0" resolved "https://registry.npmmirror.com/click-to-react-component/-/click-to-react-component-1.1.0.tgz#6268659153881d9e6052deee54b1716c63706ff6" @@ -12781,15 +12113,6 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -12858,7 +12181,7 @@ clsx@^1.2.1: resolved "https://registry.npmmirror.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -cluster-key-slot@1.1.2, cluster-key-slot@^1.1.0: +cluster-key-slot@1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== @@ -12899,19 +12222,6 @@ code-point-at@^1.0.0: resolved "https://registry.npmmirror.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== -codemirror@^6.0.0: - version "6.0.1" - resolved "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" - integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== - dependencies: - "@codemirror/autocomplete" "^6.0.0" - "@codemirror/commands" "^6.0.0" - "@codemirror/language" "^6.0.0" - "@codemirror/lint" "^6.0.0" - "@codemirror/search" "^6.0.0" - "@codemirror/state" "^6.0.0" - "@codemirror/view" "^6.0.0" - codepage@~1.15.0: version "1.15.0" resolved "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab" @@ -13300,13 +12610,6 @@ consola@^3.2.3: resolved "https://registry.npmmirror.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== -console-browserify@1.1.x: - version "1.1.0" - resolved "https://registry.npmmirror.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - integrity sha512-duS7VP5pvfsNLDvL1O4VOEbw37AI3A4ZUQYemvDlnpGrNu9tprR7BYWpDYwC0Xia0Zxz5ZupdiIrUp0GH1aXfg== - dependencies: - date-now "^0.1.4" - console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.npmmirror.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -13334,14 +12637,14 @@ content-disposition@0.5.2: resolved "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== -content-disposition@0.5.4, content-disposition@^0.5.4, content-disposition@~0.5.2: +content-disposition@^0.5.4, content-disposition@~0.5.2: version "0.5.4" resolved "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" -content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -13452,16 +12755,6 @@ convert-source-map@^2.0.0: resolved "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.7.1: - version "0.7.1" - resolved "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" - integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== - cookie@~0.4.1: version "0.4.2" resolved "https://registry.npmmirror.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -13512,7 +12805,7 @@ copy-props@^2.0.1: each-props "^1.3.2" is-plain-object "^5.0.0" -copy-to-clipboard@3.3.3, copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3: +copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3: version "3.3.3" resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== @@ -13691,11 +12984,6 @@ create-require@^1.1.0: resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -crelt@^1.0.5, crelt@^1.0.6: - version "1.0.6" - resolved "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" - integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== - cron-parser@4.4.0: version "4.4.0" resolved "https://registry.npmmirror.com/cron-parser/-/cron-parser-4.4.0.tgz#829d67f9e68eb52fa051e62de0418909f05db983" @@ -13799,11 +13087,6 @@ crypto-random-string@^1.0.0: resolved "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" integrity sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg== -crypto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" - integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== - css-blank-pseudo@^3.0.3: version "3.0.3" resolved "https://registry.npmmirror.com/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz#36523b01c12a25d812df343a32c322d2a2324561" @@ -14466,11 +13749,6 @@ date-fns@^2.29.1: dependencies: "@babel/runtime" "^7.21.0" -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.npmmirror.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - integrity sha512-AsElvov3LoNB7tf5k37H2jYSB+ZZPMT5sG2QjJCcdlV5chIv6htBUBUui2IKRjgtKAKtCBN7Zbwa+MtwLjSeNw== - dateformat@^2.0.0: version "2.2.0" resolved "https://registry.npmmirror.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" @@ -14733,11 +14011,6 @@ default-browser-id@^3.0.0: bplist-parser "^0.2.0" untildify "^4.0.0" -default-browser-id@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" - integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== - default-browser@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" @@ -14748,14 +14021,6 @@ default-browser@^4.0.0: execa "^7.1.1" titleize "^3.0.0" -default-browser@^5.2.1: - version "5.2.1" - resolved "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" - integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== - dependencies: - bundle-name "^4.1.0" - default-browser-id "^5.0.0" - default-compare@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f" @@ -15009,11 +14274,6 @@ digest-header@^1.0.0: resolved "https://registry.npmmirror.com/digest-header/-/digest-header-1.1.0.tgz#e16ab6cf4545bc4eea878c8c35acd1b89664d800" integrity sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg== -dijkstrajs@^1.0.1: - version "1.0.3" - resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" - integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -15101,13 +14361,6 @@ domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: resolved "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== -domhandler@2.3: - version "2.3.0" - resolved "https://registry.npmmirror.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" - integrity sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ== - dependencies: - domelementtype "1" - domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: version "4.3.1" resolved "https://registry.npmmirror.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" @@ -15127,14 +14380,6 @@ dompurify@2.4.3: resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== -domutils@1.5: - version "1.5.1" - resolved "https://registry.npmmirror.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" - integrity sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw== - dependencies: - dom-serializer "0" - domelementtype "1" - domutils@^1.7.0: version "1.7.0" resolved "https://registry.npmmirror.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -15563,11 +14808,6 @@ enquirer@2.3.6: dependencies: ansi-colors "^4.1.1" -entities@1.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" - integrity sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ== - entities@^2.0.0: version "2.2.0" resolved "https://registry.npmmirror.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -16470,7 +15710,7 @@ eventemitter3@^5.0.1: resolved "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== -events@3.3.0, events@^3.0.0, events@^3.3.0: +events@3.3.0, events@^3.0.0: version "3.3.0" resolved "https://registry.npmmirror.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -16599,11 +15839,6 @@ exit-on-epipe@~1.0.1: resolved "https://registry.npmmirror.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== -exit@0.1.2, exit@0.1.x: - version "0.1.2" - resolved "https://registry.npmmirror.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.npmmirror.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -16636,43 +15871,6 @@ exponential-backoff@^3.1.1: resolved "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express@^4.19.2: - version "4.21.2" - resolved "https://registry.npmmirror.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" - integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.3" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.7.1" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~2.0.0" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.3.1" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.3" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.12" - proxy-addr "~2.0.7" - qs "6.13.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.19.0" - serve-static "1.16.2" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - ext@^1.1.2: version "1.7.0" resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -17039,19 +16237,6 @@ finalhandler@1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - dependencies: - debug "2.6.9" - encodeurl "~2.0.0" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - find-cache-dir@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" @@ -17389,11 +16574,6 @@ formstream@^1.1.0: mime "^2.5.2" pause-stream "~0.0.11" -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - frac@~1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" @@ -17629,7 +16809,7 @@ get-caller-file@^1.0.1: resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -18939,17 +18119,6 @@ html5-qrcode@^2.3.8: resolved "https://registry.npmmirror.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d" integrity sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ== -htmlparser2@3.8.x: - version "3.8.3" - resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" - integrity sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q== - dependencies: - domelementtype "1" - domhandler "2.3" - domutils "1.5" - entities "1.0" - readable-stream "1.1" - htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -19080,14 +18249,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -https-proxy-agent@^7.0.0, https-proxy-agent@^7.0.3: - version "7.0.6" - resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" - integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== - dependencies: - agent-base "^7.1.2" - debug "4" - https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.5: version "7.0.5" resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" @@ -19096,6 +18257,14 @@ https-proxy-agent@^7.0.2, https-proxy-agent@^7.0.5: agent-base "^7.0.2" debug "4" +https-proxy-agent@^7.0.3: + version "7.0.6" + resolved "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + httpx@^2.2.0, httpx@^2.2.6: version "2.3.1" resolved "https://registry.npmmirror.com/httpx/-/httpx-2.3.1.tgz#e4c7a07dd7f9458810f3ae80eb13e3afcc75500b" @@ -19104,14 +18273,6 @@ httpx@^2.2.0, httpx@^2.2.6: "@types/node" "^20" debug "^4.1.1" -httpx@^2.3.2, httpx@^2.3.3: - version "2.3.3" - resolved "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz#353d3d9161b7cc2be4c638873f2fe6b319354c89" - integrity sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw== - dependencies: - "@types/node" "^20" - debug "^4.1.1" - human-signals@^1.1.1: version "1.1.1" resolved "https://registry.npmmirror.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -19184,7 +18345,7 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1: +ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -19464,33 +18625,6 @@ invert-kv@^1.0.0: resolved "https://registry.npmmirror.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" integrity sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ== -ioredis@^5.4.1: - version "5.6.1" - resolved "https://registry.npmmirror.com/ioredis/-/ioredis-5.6.1.tgz#1ed7dc9131081e77342503425afceaf7357ae599" - integrity sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA== - dependencies: - "@ioredis/commands" "^1.1.1" - cluster-key-slot "^1.1.0" - debug "^4.3.4" - denque "^2.1.0" - lodash.defaults "^4.2.0" - lodash.isarguments "^3.1.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -ip-range-check@^0.2.0: - version "0.2.0" - resolved "https://registry.npmmirror.com/ip-range-check/-/ip-range-check-0.2.0.tgz#e67f126c8fb36c8f11d4c07d7924b7e364365157" - integrity sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw== - dependencies: - ipaddr.js "^1.0.1" - -ip-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632" - integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== - ip@^1.1.4, ip@^1.1.5, ip@^1.1.8: version "1.1.8" resolved "https://registry.npmmirror.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" @@ -19501,11 +18635,6 @@ ip@^2.0.0: resolved "https://registry.npmmirror.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== -ipaddr.js@1.9.1, ipaddr.js@^1.0.1: - version "1.9.1" - resolved "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - is-absolute@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" @@ -20197,13 +19326,6 @@ is-wsl@^2.1.1, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -is-wsl@^3.1.0: - version "3.1.0" - resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" - integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== - dependencies: - is-inside-container "^1.0.0" - is-yarn-global@^0.3.0: version "0.3.0" resolved "https://registry.npmmirror.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" @@ -20444,11 +19566,6 @@ jiti@^1.20.0: resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== -jmespath@^0.16.0: - version "0.16.0" - resolved "https://registry.npmmirror.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" - integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== - joycon@^3.1.1: version "3.1.1" resolved "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" @@ -20474,11 +19591,6 @@ js-git@^0.7.8: git-sha1 "^0.1.2" pako "^0.2.5" -js-md4@^0.3.2: - version "0.3.2" - resolved "https://registry.npmmirror.com/js-md4/-/js-md4-0.3.2.tgz#cd3b3dc045b0c404556c81ddb5756c23e59d7cf5" - integrity sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA== - js-string-escape@1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -20556,11 +19668,6 @@ jsdom@^25.0.1: ws "^8.18.0" xml-name-validator "^5.0.0" -jsep@^1.3.8, jsep@^1.4.0: - version "1.4.0" - resolved "https://registry.npmmirror.com/jsep/-/jsep-1.4.0.tgz#19feccbfa51d8a79f72480b4b8e40ce2e17152f0" - integrity sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw== - jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -20581,19 +19688,6 @@ jsesc@~3.0.2: resolved "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== -jshint@^2.13.6: - version "2.13.6" - resolved "https://registry.npmmirror.com/jshint/-/jshint-2.13.6.tgz#3679a2687a3066fa9034ef85d8c305613a31eec6" - integrity sha512-IVdB4G0NTTeQZrBoM8C5JFVLjV2KtZ9APgybDA1MK73xb09qFs0jCXyQLnCOp1cSZZZbvhq/6mfXHUTaDkffuQ== - dependencies: - cli "~1.0.0" - console-browserify "1.1.x" - exit "0.1.x" - htmlparser2 "3.8.x" - lodash "~4.17.21" - minimatch "~3.0.2" - strip-json-comments "1.0.x" - json-bigint@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" @@ -20698,11 +19792,6 @@ json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: resolved "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonata@^2.0.3: - version "2.0.6" - resolved "https://registry.npmmirror.com/jsonata/-/jsonata-2.0.6.tgz#1187ec97f4662d6857b1cc40770f3ea61345d04c" - integrity sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ== - jsonc-parser@^3.2.0: version "3.2.0" resolved "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" @@ -20741,25 +19830,7 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonpath-plus@^10.3.0: - version "10.3.0" - resolved "https://registry.npmmirror.com/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz#59e22e4fa2298c68dfcd70659bb47f0cad525238" - integrity sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA== - dependencies: - "@jsep-plugin/assignment" "^1.3.0" - "@jsep-plugin/regex" "^1.0.4" - jsep "^1.4.0" - -jsonpath-plus@^9.0.0: - version "9.0.0" - resolved "https://registry.npmmirror.com/jsonpath-plus/-/jsonpath-plus-9.0.0.tgz#bb8703ee481531142bca8dee9a42fe72b8358a7f" - integrity sha512-bqE77VIDStrOTV/czspZhTn+o27Xx9ZJRGVkdVShEtPoqsIx5yALv3lWVU6y+PqYvWPJNWE7ORCQheQkEe0DDA== - dependencies: - "@jsep-plugin/assignment" "^1.2.1" - "@jsep-plugin/regex" "^1.0.3" - jsep "^1.3.8" - -jsonwebtoken@^9.0.0, jsonwebtoken@^9.0.2: +jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== @@ -21501,11 +20572,6 @@ lodash.indexof@^4.0.5: resolved "https://registry.npmmirror.com/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c" integrity sha512-t9wLWMQsawdVmf6/IcAgVGqAJkNzYVcn4BHYZKTPW//l7N5Oq7Bq138BaVk19agcsPZePcidSgTTw4NqS1nUAw== -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== - lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -21601,7 +20667,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== -lodash@4.17.21, lodash@4.x, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.21: +lodash@4.17.21, lodash@4.x, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@^4.x: version "4.17.21" resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -21715,7 +20781,7 @@ lru-cache@^10.0.2: resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== -lru-cache@^10.2.0, lru-cache@^10.3.0: +lru-cache@^10.2.0: version "10.4.3" resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== @@ -21924,17 +20990,6 @@ mariadb@^2.5.6: moment-timezone "^0.5.34" please-upgrade-node "^3.2.0" -mariadb@^3.3.0: - version "3.4.2" - resolved "https://registry.npmmirror.com/mariadb/-/mariadb-3.4.2.tgz#9fe885f11eccb752500cdc39abde83bcbeb72340" - integrity sha512-B17vhYRHDMQ1XXvhSWsvKJbpw3Q8B6py93ThBEXZXSgxIbqnKqoHK1RzoPLbIxoEzWN3jA86ZaMMc3IG6L5wsw== - dependencies: - "@types/geojson" "^7946.0.14" - "@types/node" "^22.5.4" - denque "^2.1.0" - iconv-lite "^0.6.3" - lru-cache "^10.3.0" - markdown-it-highlightjs@3.3.1: version "3.3.1" resolved "https://registry.npmmirror.com/markdown-it-highlightjs/-/markdown-it-highlightjs-3.3.1.tgz#38403610487292b8a1ae2d1acc7bb66e4ede6be8" @@ -22458,7 +21513,7 @@ meow@^8.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" -merge-descriptors@1.0.3, merge-descriptors@^1.0.1: +merge-descriptors@^1.0.1: version "1.0.3" resolved "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== @@ -22495,7 +21550,7 @@ mermaid@9.4.3: uuid "^9.0.0" web-worker "^1.2.0" -methods@^1.1.2, methods@~1.1.2: +methods@^1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== @@ -23239,13 +22294,6 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimatch@~3.0.2: - version "3.0.8" - resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmmirror.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -23486,23 +22534,11 @@ moment-timezone@^0.5.34, moment-timezone@^0.5.40, moment-timezone@^0.5.43: dependencies: moment "^2.29.4" -moment-timezone@^0.5.45: - version "0.5.48" - resolved "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz#111727bb274734a518ae154b5ca589283f058967" - integrity sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw== - dependencies: - moment "^2.29.4" - moment@^2.29.1, moment@^2.29.4: version "2.29.4" resolved "https://registry.npmmirror.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== -moment@^2.30.1: - version "2.30.1" - resolved "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -23663,7 +22699,7 @@ nanoid@^2.1.0: resolved "https://registry.npmmirror.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== -nanoid@^3.3.11, nanoid@^3.3.6: +nanoid@^3.3.11: version "3.3.11" resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== @@ -23690,11 +22726,6 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" -native-duplexpair@^1.0.0: - version "1.0.0" - resolved "https://registry.npmmirror.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz#7899078e64bf3c8a3d732601b3d40ff05db58fa0" - integrity sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA== - natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.npmmirror.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -23760,15 +22791,6 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -nock@^13.3.3: - version "13.5.6" - resolved "https://registry.npmmirror.com/nock/-/nock-13.5.6.tgz#5e693ec2300bbf603b61dae6df0225673e6c4997" - integrity sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ== - dependencies: - debug "^4.1.0" - json-stringify-safe "^5.0.1" - propagate "^2.0.0" - node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.npmmirror.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -23886,7 +22908,7 @@ node-releases@^2.0.19: resolved "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -node-sql-parser@^4.11.0, node-sql-parser@^4.18.0: +node-sql-parser@^4.18.0: version "4.18.0" resolved "https://registry.npmmirror.com/node-sql-parser/-/node-sql-parser-4.18.0.tgz#516b6e633c55c5abbba1ca588ab372db81ae9318" integrity sha512-2YEOR5qlI1zUFbGMLKNfsrR5JUvFg9LxIRVE+xJe962pfVLH0rnItqLzv96XVs1Y1UIR8FxsXAuvX/lYAWZ2BQ== @@ -24346,13 +23368,6 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmmirror.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -ollama@^0.5.12: - version "0.5.16" - resolved "https://registry.npmmirror.com/ollama/-/ollama-0.5.16.tgz#cb695b4aab6f6c07236a04b3aee40160f4f9e892" - integrity sha512-OEbxxOIUZtdZgOaTPAULo051F5y+Z1vosxEYOoABPnQKeW7i4O8tJNlxCB+xioyoorVqgjkdj+TA1f1Hy2ug/w== - dependencies: - whatwg-fetch "^3.6.20" - omit-deep@0.3.0: version "0.3.0" resolved "https://registry.npmmirror.com/omit-deep/-/omit-deep-0.3.0.tgz#21c8af3499bcadd29651a232cbcacbc52445ebec" @@ -24425,16 +23440,6 @@ only@~0.0.2: resolved "https://registry.npmmirror.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ== -open@^10.1.0: - version "10.1.2" - resolved "https://registry.npmmirror.com/open/-/open-10.1.2.tgz#d5df40984755c9a9c3c93df8156a12467e882925" - integrity sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw== - dependencies: - default-browser "^5.2.1" - define-lazy-prop "^3.0.0" - is-inside-container "^1.0.0" - is-wsl "^3.1.0" - open@^6.3.0: version "6.4.0" resolved "https://registry.npmmirror.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" @@ -24603,15 +23608,6 @@ osx-release@^1.0.0: dependencies: minimist "^1.1.0" -otplib@^12.0.1: - version "12.0.1" - resolved "https://registry.npmmirror.com/otplib/-/otplib-12.0.1.tgz#c1d3060ab7aadf041ed2960302f27095777d1f73" - integrity sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg== - dependencies: - "@otplib/core" "^12.0.1" - "@otplib/preset-default" "^12.0.1" - "@otplib/preset-v11" "^12.0.1" - p-all@3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/p-all/-/p-all-3.0.0.tgz#077c023c37e75e760193badab2bad3ccd5782bfb" @@ -25179,11 +24175,6 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@0.1.12: - version "0.1.12" - resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" - integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== - path-to-regexp@1.7.0: version "1.7.0" resolved "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" @@ -25302,17 +24293,12 @@ pg-pool@^3.6.1: resolved "https://registry.npmmirror.com/pg-pool/-/pg-pool-3.6.1.tgz#5a902eda79a8d7e3c928b77abf776b3cb7d351f7" integrity sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og== -pg-protocol@*: - version "1.10.0" - resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.10.0.tgz#a473afcbb1c6e5dc3ac24869ba3dd563f8a1ae1b" - integrity sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q== - pg-protocol@^1.6.0: version "1.6.0" resolved "https://registry.npmmirror.com/pg-protocol/-/pg-protocol-1.6.0.tgz#4c91613c0315349363af2084608db843502f8833" integrity sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q== -pg-types@^2.1.0, pg-types@^2.2.0: +pg-types@^2.1.0: version "2.2.0" resolved "https://registry.npmmirror.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== @@ -25597,11 +24583,6 @@ pm2@^6.0.5: optionalDependencies: pm2-sysmonit "^1.2.8" -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== - point-in-polygon@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz#b0af2616c01bdee341cbf2894df643387ca03357" @@ -26446,11 +25427,6 @@ prop-types@^15.5.8, prop-types@^15.7.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -propagate@^2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" - integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== - property-information@^5.0.0: version "5.6.0" resolved "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69" @@ -26490,14 +25466,6 @@ protoduck@^4.0.0: dependencies: genfun "^4.0.1" -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - proxy-agent@~6.4.0: version "6.4.0" resolved "https://registry.npmmirror.com/proxy-agent/-/proxy-agent-6.4.0.tgz#b4e2dd51dee2b377748aef8d45604c2d7608652d" @@ -26597,15 +25565,6 @@ q@^1.1.2, q@^1.5.1: resolved "https://registry.npmmirror.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== -qrcode@^1.5.4: - version "1.5.4" - resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" - integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== - dependencies: - dijkstrajs "^1.0.1" - pngjs "^5.0.0" - yargs "^15.3.1" - qs@6.13.0: version "6.13.0" resolved "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" @@ -27707,16 +26666,6 @@ read@1, read@^1.0.4, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.1: - version "1.1.13" - resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" - integrity sha512-E98tWzqShvKDGpR2MbjsDkDQWLW2TfWUC15H4tNQhIJ5Lsta84l8nUGL9/ybltGwe+wZzWPpc1Kmd2wQP4bdCA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - "readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -27726,17 +26675,6 @@ readable-stream@1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^4.2.0: - version "4.7.0" - resolved "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" - integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - readdir-glob@^1.1.2: version "1.1.3" resolved "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" @@ -27805,18 +26743,6 @@ redent@^4.0.0: indent-string "^5.0.0" strip-indent "^4.0.0" -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== - dependencies: - redis-errors "^1.0.0" - redis@^4.6.10, redis@^4.6.7: version "4.6.11" resolved "https://registry.npmmirror.com/redis/-/redis-4.6.11.tgz#fad85e104545228f212259fd557c3e4f8eafcd3d" @@ -27829,13 +26755,6 @@ redis@^4.6.10, redis@^4.6.7: "@redis/search" "1.1.6" "@redis/time-series" "1.0.5" -redlock@^5.0.0-beta.2: - version "5.0.0-beta.2" - resolved "https://registry.npmmirror.com/redlock/-/redlock-5.0.0-beta.2.tgz#a629c07e07d001c0fdd9f2efa614144c4416fe44" - integrity sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw== - dependencies: - node-abort-controller "^3.0.1" - reduce-configs@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/reduce-configs/-/reduce-configs-1.1.0.tgz#6601bc10bbe60ec0900763c67680d56e3e9d356e" @@ -28319,11 +27238,6 @@ require-main-filename@^1.0.1: resolved "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug== -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - requireindex@^1.2.0: version "1.2.0" resolved "https://registry.npmmirror.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" @@ -28585,11 +27499,6 @@ run-applescript@^5.0.0: dependencies: execa "^5.0.0" -run-applescript@^7.0.0: - version "7.0.0" - resolved "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" - integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== - run-async@^2.2.0, run-async@^2.4.0: version "2.4.1" resolved "https://registry.npmmirror.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -28950,28 +27859,6 @@ sequelize@^6.26.0: validator "^13.9.0" wkx "^0.5.0" -sequelize@^6.35.0: - version "6.37.7" - resolved "https://registry.npmmirror.com/sequelize/-/sequelize-6.37.7.tgz#55a6f8555ae76c1fbd4bce76b2ac5fcc0a1e6eb6" - integrity sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA== - dependencies: - "@types/debug" "^4.1.8" - "@types/validator" "^13.7.17" - debug "^4.3.4" - dottie "^2.0.6" - inflection "^1.13.4" - lodash "^4.17.21" - moment "^2.29.4" - moment-timezone "^0.5.43" - pg-connection-string "^2.6.1" - retry-as-promised "^7.0.4" - semver "^7.5.4" - sequelize-pool "^7.1.0" - toposort-class "^1.0.1" - uuid "^8.3.2" - validator "^13.9.0" - wkx "^0.5.0" - serve-handler@6.1.6, serve-handler@^6.1.6: version "6.1.6" resolved "https://registry.npmmirror.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" @@ -29606,11 +28493,6 @@ sprintf-js@1.1.2: resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -29689,11 +28571,6 @@ staged-components@^1.1.3: resolved "https://registry.npmmirror.com/staged-components/-/staged-components-1.1.3.tgz#bb5a396df2d9b48fbc31841a59f53437ed8b8ac6" integrity sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A== -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - static-extend@^0.1.1: version "0.1.2" resolved "https://registry.npmmirror.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -29943,18 +28820,13 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.3.0: +string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -30072,11 +28944,6 @@ strip-indent@^4.0.0: dependencies: min-indent "^1.0.1" -strip-json-comments@1.0.x: - version "1.0.4" - resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" - integrity sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg== - strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -30123,11 +28990,6 @@ style-loader@^3.3.3: resolved "https://registry.npmmirror.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== -style-mod@^4.0.0, style-mod@^4.1.0: - version "4.1.2" - resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" - integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== - style-to-js@^1.0.0: version "1.1.16" resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz#e6bd6cd29e250bcf8fa5e6591d07ced7575dbe7a" @@ -30501,18 +29363,6 @@ tar@^6.0.2, tar@^6.1.0: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.13: - version "6.2.1" - resolved "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - tar@^7.4.3: version "7.4.3" resolved "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" @@ -30525,22 +29375,6 @@ tar@^7.4.3: mkdirp "^3.0.1" yallist "^5.0.0" -tedious@^18.2.4: - version "18.6.1" - resolved "https://registry.npmmirror.com/tedious/-/tedious-18.6.1.tgz#1c4a3f06c891be67a032117e2e25193286d44496" - integrity sha512-9AvErXXQTd6l7TDd5EmM+nxbOGyhnmdbp/8c3pw+tjaiSXW9usME90ET/CRG1LN1Y9tPMtz/p83z4Q97B4DDpw== - dependencies: - "@azure/core-auth" "^1.7.2" - "@azure/identity" "^4.2.1" - "@azure/keyvault-keys" "^4.4.0" - "@js-joda/core" "^5.6.1" - "@types/node" ">=18" - bl "^6.0.11" - iconv-lite "^0.6.3" - js-md4 "^0.3.2" - native-duplexpair "^1.0.0" - sprintf-js "^1.1.3" - temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -30630,11 +29464,6 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" -thirty-two@^1.0.2: - version "1.0.2" - resolved "https://registry.npmmirror.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a" - integrity sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA== - thread-stream@^0.15.1: version "0.15.2" resolved "https://registry.npmmirror.com/thread-stream/-/thread-stream-0.15.2.tgz#fb95ad87d2f1e28f07116eb23d85aba3bc0425f4" @@ -31089,11 +29918,6 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3 resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tslib@^2.2.0: - version "2.8.1" - resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - tsscmp@1.0.6: version "1.0.6" resolved "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" @@ -31399,6 +30223,11 @@ typescript@^4.4.3: resolved "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@^5.x: + version "5.8.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + ua-parser-js@^1.0.33: version "1.0.38" resolved "https://registry.npmmirror.com/ua-parser-js/-/ua-parser-js-1.0.38.tgz#66bb0c4c0e322fe48edfe6d446df6042e62f25e2" @@ -31439,6 +30268,13 @@ uid-number@0.0.6: resolved "https://registry.npmmirror.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" integrity sha512-c461FXIljswCuscZn67xq9PpszkPT6RjheWFQTgCyabJrTUozElanb0YEqv2UGgk247YpcJkFBuSGNvBlpXM9w== +uid@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" + integrity sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g== + dependencies: + "@lukeed/csprng" "^1.0.0" + umask@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" @@ -31542,11 +30378,6 @@ undici-types@~6.20.0: resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== -undici-types@~6.21.0: - version "6.21.0" - resolved "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" - integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== - unescape@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96" @@ -32389,11 +31220,6 @@ void-elements@3.1.0: resolved "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -w3c-keyname@^2.2.4: - version "2.2.8" - resolved "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" - integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== - w3c-xmlserializer@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" @@ -32494,11 +31320,6 @@ whatwg-encoding@^3.1.1: dependencies: iconv-lite "0.6.3" -whatwg-fetch@^3.6.20: - version "3.6.20" - resolved "https://registry.npmmirror.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" - integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== - whatwg-mimetype@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" @@ -32582,11 +31403,6 @@ which-module@^1.0.0: resolved "https://registry.npmmirror.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" integrity sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ== -which-module@^2.0.0: - version "2.0.1" - resolved "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" - integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== - which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: version "1.1.13" resolved "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" @@ -32767,7 +31583,7 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" -wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: +wrap-ansi@^6.0.1: version "6.2.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== @@ -32901,23 +31717,6 @@ xlsx@^0.17.0: version "0.20.2" resolved "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz#0f64eeed3f1a46e64724620c3553f2dbd3cd2d7d" -xml-crypto@^3.0.1: - version "3.2.1" - resolved "https://registry.npmmirror.com/xml-crypto/-/xml-crypto-3.2.1.tgz#df2511a95275b43e18924693ff932af7de3217a4" - integrity sha512-0GUNbPtQt+PLMsC5HoZRONX+K6NBJEqpXe/lsvrFj0EqfpGPpVfJKGE7a5jCg8s2+Wkrf/2U1G41kIH+zC9eyQ== - dependencies: - "@xmldom/xmldom" "^0.8.8" - xpath "0.0.32" - -xml-encryption@^3.0.2: - version "3.1.0" - resolved "https://registry.npmmirror.com/xml-encryption/-/xml-encryption-3.1.0.tgz#f3e91c4508aafd0c21892151ded91013dcd51ca2" - integrity sha512-PV7qnYpoAMXbf1kvQkqMScLeQpjCMixddAKq9PtqVrho8HnYbBOWNfG0kA4R7zxQDo7w9kiYAyzS/ullAyO55Q== - dependencies: - "@xmldom/xmldom" "^0.8.5" - escape-html "^1.0.3" - xpath "0.0.32" - xml-lexer@^0.2.2: version "0.2.2" resolved "https://registry.npmmirror.com/xml-lexer/-/xml-lexer-0.2.2.tgz#518193a4aa334d58fc7d248b549079b89907e046" @@ -32938,14 +31737,6 @@ xml-reader@2.4.3: eventemitter3 "^2.0.0" xml-lexer "^0.2.2" -xml2js@^0.5.0: - version "0.5.0" - resolved "https://registry.npmmirror.com/xml2js/-/xml2js-0.5.0.tgz#d9440631fbb2ed800203fad106f2724f62c493b7" - integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - xml2js@^0.6.0, xml2js@^0.6.2: version "0.6.2" resolved "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" @@ -32954,11 +31745,6 @@ xml2js@^0.6.0, xml2js@^0.6.2: sax ">=0.6.0" xmlbuilder "~11.0.0" -xmlbuilder@^15.1.1: - version "15.1.1" - resolved "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" - integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== - xmlbuilder@~11.0.0: version "11.0.1" resolved "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" @@ -32969,16 +31755,6 @@ xmlchars@^2.2.0: resolved "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xpath@0.0.27: - version "0.0.27" - resolved "https://registry.npmmirror.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" - integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== - -xpath@0.0.32: - version "0.0.32" - resolved "https://registry.npmmirror.com/xpath/-/xpath-0.0.32.tgz#1b73d3351af736e17ec078d6da4b8175405c48af" - integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== - xpipe@^1.0.5: version "1.0.7" resolved "https://registry.npmmirror.com/xpipe/-/xpipe-1.0.7.tgz#d0aff00e080a44ffbdbe45dd7658ff6c483464c8" @@ -33059,14 +31835,6 @@ yargs-parser@20.2.4: resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -33085,23 +31853,6 @@ yargs-parser@^5.0.1: camelcase "^3.0.0" object.assign "^4.1.0" -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yargs@^16.2.0: version "16.2.0" resolved "https://registry.npmmirror.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" @@ -33165,14 +31916,6 @@ yauzl@^2.4.2: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yauzl@^3.2.0: - version "3.2.0" - resolved "https://registry.npmmirror.com/yauzl/-/yauzl-3.2.0.tgz#7b6cb548f09a48a6177ea0be8ece48deb7da45c0" - integrity sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w== - dependencies: - buffer-crc32 "~0.2.3" - pend "~1.2.0" - ylru@^1.2.0: version "1.3.2" resolved "https://registry.npmmirror.com/ylru/-/ylru-1.3.2.tgz#0de48017473275a4cbdfc83a1eaf67c01af8a785" @@ -33207,7 +31950,7 @@ zod-to-json-schema@^3.22.3: resolved "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz#5958ba111d681f8d01c5b6b647425c9b8a6059e7" integrity sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A== -zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.24.1: +zod-to-json-schema@^3.22.4: version "3.24.5" resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3" integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== @@ -33219,7 +31962,7 @@ zod@^3.22.4, zod@^3.24.1: zrender@5.6.1: version "5.6.1" - resolved "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz#e08d57ecf4acac708c4fcb7481eb201df7f10a6b" + resolved "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz#e08d57ecf4acac708c4fcb7481eb201df7f10a6b" integrity sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag== dependencies: tslib "2.3.0"