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 index 60ba6fb19a..955bdf0eb5 100644 --- 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 @@ -111,6 +111,7 @@ console.log(apiResource.getData()); - `setSourceId(sourceId) / getSourceId()`: 设置/获取源对象 ID。 - `setDataSourceKey(dataSourceKey) / getDataSourceKey()`: 设置/获取数据源标识(通过 header)。 - `setFilter(filter) / getFilter()`: 设置/获取过滤条件。 +- `addFilterGroup(key, filter) / removeFilterGroup(key)`: 设置/移除条件组。 - `setAppends(appends) / getAppends()`: 设置/获取附加字段。 - `addAppends(appends) / removeAppends(appends)`: 添加/移除附加字段。 - `setFilterByTk(filterByTk) / getFilterByTk()`: 设置/获取主键过滤条件。 diff --git a/packages/core/client/src/flow/FlowPage.tsx b/packages/core/client/src/flow/FlowPage.tsx index 2b3ec8083b..502051b5db 100644 --- a/packages/core/client/src/flow/FlowPage.tsx +++ b/packages/core/client/src/flow/FlowPage.tsx @@ -18,26 +18,26 @@ function InternalFlowPage({ uid, sharedContext }) { return ; } -export const FlowPage = () => { +export const FlowRoute = () => { const params = useParams(); - return ; + return ; }; -export const FlowPageComponent = (props) => { +export const FlowPage = (props) => { const { uid, parentId, sharedContext } = props; const flowEngine = useFlowEngine(); const { loading, data } = useRequest( async () => { const options = { uid, - use: 'PageFlowModel', + use: 'PageModel', subModels: { tabs: [ { - use: 'PageTabFlowModel', + use: 'PageTabModel', subModels: { grid: { - use: 'BlockGridFlowModel', + use: 'BlockGridModel', }, }, }, diff --git a/packages/core/client/src/flow/actions/openModeAction.tsx b/packages/core/client/src/flow/actions/openModeAction.tsx index a226b13946..1b253d4065 100644 --- a/packages/core/client/src/flow/actions/openModeAction.tsx +++ b/packages/core/client/src/flow/actions/openModeAction.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FlowPageComponent } from '../FlowPage'; +import { FlowPage } from '../FlowPage'; export const openModeAction = { title: '打开方式', @@ -39,7 +39,7 @@ export const openModeAction = { function DrawerContent() { return (
- .ant-space-item:first-child, - & > .ant-space-item:last-child { - flex-shrink: 0; - } - `, - }, - properties: { - name: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': 'Input', - 'x-component-props': { - placeholder: `{{t("Name")}}`, - }, - }, - value: { - type: 'string', - 'x-decorator': 'FormItem', - 'x-component': Variable.TextArea, - 'x-component-props': { - placeholder: `{{t("Value")}}`, - useTypedConstant: true, - changeOnSelect: true, - }, - }, - remove: { - type: 'void', - 'x-decorator': 'FormItem', - 'x-component': 'ArrayItems.Remove', - }, - }, - }, - }, - }, - properties: { - add: { - type: 'void', - title: 'Add parameter', - 'x-component': 'ArrayItems.Addition', - }, - }, - }, - openInNewWindow: { - type: 'boolean', - 'x-content': 'Open in new window', - 'x-decorator': 'FormItem', - 'x-component': 'Checkbox', - }, - }, - handler(ctx, params) { - ctx.globals.modal.confirm({ - title: `${ctx.extra.currentRecord?.id}`, - content: JSON.stringify(params, null, 2), - }); - }, - }, - }, -}); diff --git a/packages/core/client/src/flow/models/SubmitActionModel.tsx b/packages/core/client/src/flow/models/SubmitActionModel.tsx deleted file mode 100644 index f10f99cef7..0000000000 --- a/packages/core/client/src/flow/models/SubmitActionModel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { Submit } from '@formily/antd-v5'; -import React from 'react'; -import { ActionModel } from './ActionModel'; - -export class SubmitActionModel extends ActionModel { - render() { - return {this.props.title || 'Submit'}; - } -} - -SubmitActionModel.registerFlow({ - key: 'event1', - on: { - eventName: 'click', - }, - steps: { - step1: { - async handler(ctx, params) { - if (ctx.extra.currentModel) { - await ctx.extra.currentModel.form.submit(); - const values = ctx.extra.currentModel.form.values; - await ctx.extra.currentModel.resource.save(values); - } - if (ctx.shared.currentDrawer) { - ctx.shared.currentDrawer.destroy(); - } - if (ctx.shared.currentResource) { - ctx.shared.currentResource.refresh(); - } - }, - }, - }, -}); diff --git a/packages/core/client/src/flow/models/TableColumnModel.tsx b/packages/core/client/src/flow/models/TableColumnModel.tsx deleted file mode 100644 index 84f3a9b83a..0000000000 --- a/packages/core/client/src/flow/models/TableColumnModel.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { EditOutlined, SettingOutlined } from '@ant-design/icons'; -import { css } from '@emotion/css'; -import { observer } from '@formily/reactive-react'; -import { AddActionModel, CollectionField, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine'; -import { Space, TableColumnProps, Tooltip } from 'antd'; -import React from 'react'; -import { ActionModel } from './ActionModel'; -import { FieldFlowModel, SupportedFieldInterfaces } from './FieldFlowModel'; -import { QuickEditForm } from './QuickEditForm'; - -export class TableColumnModel extends FieldFlowModel { - static readonly supportedFieldInterfaces: SupportedFieldInterfaces = '*'; - - getColumnProps(): TableColumnProps { - return { - ...this.props, - title: ( - - {this.props.title} - - ), - ellipsis: true, - onCell: (record) => ({ - className: css` - .edit-icon { - position: absolute; - display: none; - color: #1890ff; - margin-left: 8px; - cursor: pointer; - top: 50%; - right: 8px; - transform: translateY(-50%); - } - &:hover { - background: rgba(24, 144, 255, 0.1) !important; - } - &:hover .edit-icon { - display: inline-flex; - } - `, - }), - render: this.render(), - }; - } - - renderQuickEditButton(record) { - return ( - - { - e.stopPropagation(); - await QuickEditForm.open({ - flowEngine: this.flowEngine, - collectionField: this.field as CollectionField, - filterByTk: record.id, - }); - await this.parent.resource.refresh(); - }} - /> - - ); - } - - render() { - return (value, record, index) => ( - <> - {value} - {this.renderQuickEditButton(record)} - - ); - } -} - -TableColumnModel.define({ - title: 'Table Column', - icon: 'TableColumn', - defaultOptions: { - use: 'TableColumnModel', - }, - sort: 0, -}); - -const Columns = observer(({ record, model, index }) => { - return ( - - {model.mapSubModels('actions', (action: ActionModel) => { - const fork = action.createFork({}, `${index}`); - return ( - - ); - })} - - ); -}); - -export class TableActionsColumnModel extends TableColumnModel { - static readonly supportedFieldInterfaces: SupportedFieldInterfaces = null; - - getColumnProps() { - return { - // title: 'Actions', - ...this.props, - title: ( - - - {this.props.title || 'Actions'} - [ - { - key: 'view', - label: 'View', - createModelOptions: { - use: 'ViewActionModel', - }, - }, - { - key: 'link', - label: 'Link', - createModelOptions: { - use: 'LinkActionModel', - }, - }, - { - key: 'delete', - label: 'Delete', - createModelOptions: { - use: 'DeleteActionModel', - }, - }, - { - key: 'popup', - label: 'Popup', - createModelOptions: { - use: 'PopupActionModel', - }, - }, - { - key: 'edit', - label: 'Edit', - createModelOptions: { - use: 'EditActionModel', - }, - }, - { - key: 'edit', - label: 'Duplicate', - createModelOptions: { - use: 'DuplicateActionModel', - }, - }, - { - key: 'edit', - label: 'Custom Request', - createModelOptions: { - use: 'CustomRequestActionModel', - }, - }, - { - key: 'edit', - label: 'Update record', - createModelOptions: { - use: 'UpdateRecordActionModel', - }, - }, - ]} - > - - - - - ), - render: this.render(), - }; - } - - render() { - return (value, record, index) => ; - } -} - -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.field = field; - ctx.model.fieldPath = params.fieldPath; - ctx.model.setProps('title', field.title); - ctx.model.setProps('dataIndex', field.name); - }, - }, - }, -}); diff --git a/packages/core/client/src/flow/models/actions/AddNewActionModel.tsx b/packages/core/client/src/flow/models/actions/AddNewActionModel.tsx new file mode 100644 index 0000000000..240263c683 --- /dev/null +++ b/packages/core/client/src/flow/models/actions/AddNewActionModel.tsx @@ -0,0 +1,70 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { ButtonProps } from 'antd'; +import React from 'react'; +import { FlowPage } from '../../FlowPage'; +import { ActionModel } from '../base/ActionModel'; + +export class AddNewActionModel extends ActionModel { + defaultProps: ButtonProps = { + type: 'primary', + children: 'Add new', + }; +} + +AddNewActionModel.registerFlow({ + sort: 200, + title: '事件', + key: 'event1', + on: { + eventName: 'click', + }, + steps: { + step1: { + title: '弹窗配置', + uiSchema: { + width: { + type: 'number', + title: '宽度', + 'x-decorator': 'FormItem', + 'x-component': 'NumberPicker', + 'x-component-props': { + placeholder: '请输入宽度', + }, + }, + }, + defaultParams: { + width: 800, + }, + handler(ctx, params) { + // eslint-disable-next-line prefer-const + let currentDrawer: any; + + function DrawerContent() { + return ( +
+ +
+ ); + } + + currentDrawer = ctx.globals.drawer.open({ + // title: '命令式 Drawer', + header: null, + width: params.width, + content: , + }); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/BulkDeleteActionModel.tsx b/packages/core/client/src/flow/models/actions/BulkDeleteActionModel.tsx similarity index 82% rename from packages/core/client/src/flow/models/BulkDeleteActionModel.tsx rename to packages/core/client/src/flow/models/actions/BulkDeleteActionModel.tsx index 99e92e90da..9041edd294 100644 --- a/packages/core/client/src/flow/models/BulkDeleteActionModel.tsx +++ b/packages/core/client/src/flow/models/actions/BulkDeleteActionModel.tsx @@ -8,12 +8,12 @@ */ import { MultiRecordResource } from '@nocobase/flow-engine'; -import { ActionModel } from './ActionModel'; import { ButtonProps } from 'antd'; +import { ActionModel } from '../base/ActionModel'; export class BulkDeleteActionModel extends ActionModel { defaultProps: ButtonProps = { - title: 'Delete', + children: 'Delete', }; } @@ -25,11 +25,11 @@ BulkDeleteActionModel.registerFlow({ steps: { step1: { async handler(ctx, params) { - if (!ctx.extra.currentResource) { + if (!ctx.shared?.currentBlockModel?.resource) { ctx.globals.message.error('No resource selected for deletion.'); return; } - const resource = ctx.extra.currentResource as MultiRecordResource; + const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource; if (resource.getSelectedRows().length === 0) { ctx.globals.message.warning('No records selected for deletion.'); return; diff --git a/packages/core/client/src/flow/models/CustomRequestActionModel.tsx b/packages/core/client/src/flow/models/actions/CustomRequestActionModel.tsx similarity index 92% rename from packages/core/client/src/flow/models/CustomRequestActionModel.tsx rename to packages/core/client/src/flow/models/actions/CustomRequestActionModel.tsx index 1c972f5e9a..5ed0c9f0bf 100644 --- a/packages/core/client/src/flow/models/CustomRequestActionModel.tsx +++ b/packages/core/client/src/flow/models/actions/CustomRequestActionModel.tsx @@ -8,12 +8,12 @@ */ import type { ButtonProps } from 'antd/es/button'; -import { ActionModel } from './ActionModel'; -import { secondaryConfirmationAction } from '../actions/secondaryConfirmationAction'; -import { refreshOnCompleteAction } from '../actions/refreshOnCompleteAction'; -import { useAfterSuccessOptions } from '../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions'; -import { useGlobalVariable } from '../../application/hooks/useGlobalVariable'; -import { BlocksSelector } from '../../schema-component/antd/action/Action.Designer'; +import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable'; +import { BlocksSelector } from '../../../schema-component/antd/action/Action.Designer'; +import { useAfterSuccessOptions } from '../../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions'; +import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction'; +import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction'; +import { ActionModel } from '../base/ActionModel'; export class CustomRequestActionModel extends ActionModel { defaultProps: ButtonProps = { @@ -51,7 +51,8 @@ CustomRequestActionModel.registerFlow({ required: true, title: 'HTTP method', 'x-decorator-props': { - tooltip: 'When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data', + tooltip: + 'When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data', }, 'x-decorator': 'FormItem', 'x-component': 'Select', diff --git a/packages/core/client/src/flow/models/actions/DeleteActionModel.tsx b/packages/core/client/src/flow/models/actions/DeleteActionModel.tsx new file mode 100644 index 0000000000..870608506e --- /dev/null +++ b/packages/core/client/src/flow/models/actions/DeleteActionModel.tsx @@ -0,0 +1,73 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { MultiRecordResource } from '@nocobase/flow-engine'; +import type { ButtonProps } from 'antd'; +import { ActionModel } from '../base/ActionModel'; + +export class DeleteActionModel extends ActionModel { + defaultProps: ButtonProps = { + children: 'Delete', + type: 'link', + }; +} + +DeleteActionModel.registerFlow({ + key: 'event1', + on: { + eventName: 'click', + }, + steps: { + confirm: { + uiSchema: { + title: { + type: 'string', + title: 'Confirm title', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + }, + content: { + type: 'string', + title: 'Confirm content', + 'x-decorator': 'FormItem', + 'x-component': 'Input.TextArea', + }, + }, + defaultParams: { + title: 'Confirm Deletion', + content: 'Are you sure you want to delete this record?', + }, + async handler(ctx, params) { + const confirmed = await ctx.globals.modal.confirm({ + title: params.title, + content: params.content, + }); + if (!confirmed) { + ctx.globals.message.info('Deletion cancelled.'); + return ctx.exit(); + } + }, + }, + step1: { + async handler(ctx, params) { + if (!ctx.shared?.currentBlockModel?.resource) { + ctx.globals.message.error('No resource selected for deletion.'); + return; + } + if (!ctx.extra.currentRecord) { + ctx.globals.message.error('No resource or record selected for deletion.'); + return; + } + const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource; + await resource.destroy(ctx.extra.currentRecord); + ctx.globals.message.success('Record deleted successfully.'); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/DuplicateActionModel.tsx b/packages/core/client/src/flow/models/actions/DuplicateActionModel.tsx similarity index 81% rename from packages/core/client/src/flow/models/DuplicateActionModel.tsx rename to packages/core/client/src/flow/models/actions/DuplicateActionModel.tsx index 78a516161d..f75107071d 100644 --- a/packages/core/client/src/flow/models/DuplicateActionModel.tsx +++ b/packages/core/client/src/flow/models/actions/DuplicateActionModel.tsx @@ -8,11 +8,8 @@ */ import type { ButtonProps } from 'antd/es/button'; -import { ActionModel } from './ActionModel'; -import { openModeAction } from '../actions/openModeAction'; -import { findFormBlock } from '../../block-provider/FormBlockProvider'; -import { useMemo } from 'react'; -import { useSyncFromForm } from '../../schema-settings/DataTemplates/utils'; +import { openModeAction } from '../../actions/openModeAction'; +import { ActionModel } from '../base/ActionModel'; export class DuplicateActionModel extends ActionModel { defaultProps: ButtonProps = { diff --git a/packages/core/client/src/flow/models/EditActionModel.tsx b/packages/core/client/src/flow/models/actions/EditActionModel.tsx similarity index 84% rename from packages/core/client/src/flow/models/EditActionModel.tsx rename to packages/core/client/src/flow/models/actions/EditActionModel.tsx index 7f3f723387..3bb3aaaf27 100644 --- a/packages/core/client/src/flow/models/EditActionModel.tsx +++ b/packages/core/client/src/flow/models/actions/EditActionModel.tsx @@ -8,8 +8,8 @@ */ import type { ButtonProps } from 'antd/es/button'; -import { ActionModel } from './ActionModel'; -import { openModeAction } from '../actions/openModeAction'; +import { openModeAction } from '../../actions/openModeAction'; +import { ActionModel } from '../base/ActionModel'; export class EditActionModel extends ActionModel { defaultProps: ButtonProps = { diff --git a/packages/core/client/src/flow/models/actions/FilterActionModel.tsx b/packages/core/client/src/flow/models/actions/FilterActionModel.tsx new file mode 100644 index 0000000000..b620fecc84 --- /dev/null +++ b/packages/core/client/src/flow/models/actions/FilterActionModel.tsx @@ -0,0 +1,53 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { MultiRecordResource } from '@nocobase/flow-engine'; +import { Button, ButtonProps, Input, Popover } from 'antd'; +import _ from 'lodash'; +import React from 'react'; +import { ActionModel } from '../base/ActionModel'; + +export class FilterActionModel extends ActionModel { + defaultProps: ButtonProps = { + type: 'default', + children: 'Filter', + }; + + render() { + return ( + + { + const resource = this.ctx.shared?.currentBlockModel?.resource as MultiRecordResource; + if (!resource) { + return; + } + resource.addFilterGroup(this.uid, { + $or: [ + { ['nickname.$includes']: e.target.value }, + { ['email.$includes']: e.target.value }, + { ['phone.$includes']: e.target.value }, + ], + }); + resource.refresh(); + }, 500)} + /> +
+ } + trigger="click" + placement="bottom" + > + + -// -// ); -// } - -type BlockGridFlowModelStructure = { +type GridModelStructure = { subModels: { - items: BlockFlowModel[]; + items: BlockModel[]; }; }; -export class BlockGridFlowModel extends FlowModel { +export class GridModel extends FlowModel { + subModelBaseClass = 'BlockModel'; render() { return (
- +
); } } + +export class BlockGridModel extends GridModel { + subModelBaseClass = 'BlockModel'; +} diff --git a/packages/core/client/src/flow/models/PageFlowModel.tsx b/packages/core/client/src/flow/models/base/PageModel.tsx similarity index 87% rename from packages/core/client/src/flow/models/PageFlowModel.tsx rename to packages/core/client/src/flow/models/base/PageModel.tsx index d21e9359f1..3edf3d0831 100644 --- a/packages/core/client/src/flow/models/PageFlowModel.tsx +++ b/packages/core/client/src/flow/models/base/PageModel.tsx @@ -13,13 +13,13 @@ import { Button, Tabs } from 'antd'; import _ from 'lodash'; import React from 'react'; -type PageFlowModelStructure = { +type PageModelStructure = { subModels: { tabs: FlowModel[]; }; }; -export class PageFlowModel extends FlowModel { +export class PageModel extends FlowModel { addTab(tab: any) { const model = this.addSubModel('tabs', tab); model.save(); @@ -48,11 +48,11 @@ export class PageFlowModel extends FlowModel { + + {/* [ @@ -104,7 +103,7 @@ export class TableModel extends BlockFlowModel { ]} > - + */} extends FlowModel {} +export class FilterFormActionModel extends ActionModel {} diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormFieldModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormFieldModel.tsx new file mode 100644 index 0000000000..6ccd746abc --- /dev/null +++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormFieldModel.tsx @@ -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. + */ + +import { FormFieldModel } from '../../data-blocks/form/FormFieldModel'; + +// null 表示不支持任何字段接口,* 表示支持所有字段接口 +export type SupportedFieldInterfaces = string[] | '*' | null; + +export class FilterFormFieldModel extends FormFieldModel {} diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.tsx new file mode 100644 index 0000000000..2f3eb4e7d6 --- /dev/null +++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormModel.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 { FormButtonGroup, FormLayout } from '@formily/antd-v5'; +import { createForm, Form } from '@formily/core'; +import { FormProvider } from '@formily/react'; +import { AddActionButton, AddFieldButton, Collection, FlowModelRenderer } from '@nocobase/flow-engine'; +import { Card } from 'antd'; +import React from 'react'; +import { FilterBlockModel } from '../../base/BlockModel'; +import { FilterFormFieldModel } from './FilterFormFieldModel'; + +export class FilterFormModel extends FilterBlockModel { + form: Form; + collection: Collection; + + render() { + return ( + + + + {this.mapSubModels('fields', (field) => ( + + ))} + + ({ + use: fieldClass.name, + stepParams: { + default: { + step1: { + fieldPath: `${field.collection.dataSource.name}.${field.collection.name}.${field.name}`, + }, + }, + }, + })} + onModelAdded={async (fieldModel: FilterFormFieldModel) => { + const fieldInfo = fieldModel.stepParams?.field; + if (fieldInfo && typeof fieldInfo.name === 'string') { + // 如果需要设置 collectionField,可以从 collection 中获取 + const fields = this.collection.getFields(); + const field = fields.find((f) => f.name === fieldInfo.name); + if (field) { + fieldModel.collectionField = field; + } + } + }} + subModelKey="fields" + model={this} + collection={this.collection} + subModelBaseClass="FilterFormFieldModel" + /> + + {this.mapSubModels('actions', (action) => ( + + ))} + + + + + ); + } +} + +FilterFormModel.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) { + ctx.model.collection = ctx.globals.dataSourceManager.getCollection( + params.dataSourceKey, + params.collectionName, + ); + } + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx new file mode 100644 index 0000000000..d4993dd021 --- /dev/null +++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormResetActionModel.tsx @@ -0,0 +1,45 @@ +/** + * 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 { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine'; +import { ButtonProps } from 'antd'; +import { DataBlockModel } from '../../base/BlockModel'; +import { FilterFormActionModel } from './FilterFormActionModel'; + +export class FilterFormResetActionModel extends FilterFormActionModel { + defaultProps: ButtonProps = { + children: 'Reset', + }; +} + +FilterFormResetActionModel.registerFlow({ + key: 'event1', + on: { + eventName: 'click', + }, + steps: { + step1: { + async handler(ctx, params) { + if (!ctx.shared?.currentBlockModel?.form) { + ctx.globals.message.error('No form available for reset.'); + return; + } + const currentBlockModel = ctx.shared.currentBlockModel; + await currentBlockModel.form.reset(); + const flowEngine = ctx.globals.flowEngine as FlowEngine; + flowEngine.forEachModel((model: DataBlockModel) => { + if (model.resource && model?.collection?.name === currentBlockModel.collection.name) { + (model.resource as MultiRecordResource).removeFilterGroup(currentBlockModel.uid); + (model.resource as MultiRecordResource).refresh(); + } + }); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx new file mode 100644 index 0000000000..793ba0f4cc --- /dev/null +++ b/packages/core/client/src/flow/models/filter-blocks/form/FilterFormSubmitActionModel.tsx @@ -0,0 +1,48 @@ +/** + * 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 { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine'; +import type { ButtonProps, ButtonType } from 'antd/es/button'; +import { ActionModel } from '../../base/ActionModel'; +import { DataBlockModel } from '../../base/BlockModel'; +import { FilterFormActionModel } from './FilterFormActionModel'; + +export class FilterFormSubmitActionModel extends FilterFormActionModel { + defaultProps: ButtonProps = { + children: 'Filter', + type: 'primary', + }; +} + +FilterFormSubmitActionModel.registerFlow({ + key: 'event1', + on: { + eventName: 'click', + }, + steps: { + step1: { + async handler(ctx, params) { + if (!ctx.shared?.currentBlockModel?.form) { + ctx.globals.message.error('No form available for submission.'); + return; + } + const currentBlockModel = ctx.shared.currentBlockModel; + await currentBlockModel.form.submit(); + const values = currentBlockModel.form.values; + const flowEngine = ctx.globals.flowEngine as FlowEngine; + flowEngine.forEachModel((model: DataBlockModel) => { + if (model.resource && model?.collection?.name === currentBlockModel.collection.name) { + (model.resource as MultiRecordResource).addFilterGroup(currentBlockModel.uid, values); + (model.resource as MultiRecordResource).refresh(); + } + }); + }, + }, + }, +}); diff --git a/packages/core/client/src/flow/models/index.ts b/packages/core/client/src/flow/models/index.ts index 0cb95affde..ff1f8d1b12 100644 --- a/packages/core/client/src/flow/models/index.ts +++ b/packages/core/client/src/flow/models/index.ts @@ -7,26 +7,30 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -export * from './ActionModel'; -export * from './AddNewActionModel'; -export * from './BlockFlowModel'; -export * from './BlockGridFlowModel'; -export * from './BulkDeleteActionModel'; -export * from './CalendarBlockFlowModel'; -export * from './DeleteActionModel'; -export * from './FormFieldModel'; -export * from './FormModel'; -export * from './HtmlBlockFlowModel'; -export * from './LinkActionModel'; -export * from './PageFlowModel'; -export * from './PageTabFlowModel'; -export * from './QuickEditForm'; -export * from './SubmitActionModel'; -export * from './TableColumnModel'; -export * from './TableModel'; -export * from './ViewActionModel'; -export * from './PopupActionModel'; -export * from './EditActionModel'; -export * from './DuplicateActionModel'; -export * from './CustomRequestActionModel'; -export * from './UpdateRecordActionModel'; +export * from './actions/AddNewActionModel'; +export * from './actions/BulkDeleteActionModel'; +export * from './actions/DeleteActionModel'; +export * from './actions/FilterActionModel'; +export * from './actions/LinkActionModel'; +export * from './actions/RefreshActionModel'; +export * from './actions/ViewActionModel'; +export * from './base/ActionModel'; +export * from './base/BlockModel'; +export * from './base/GridModel'; +export * from './base/PageModel'; +export * from './base/PageTabModel'; +export * from './data-blocks/calendar/CalendarBlockModel'; +export * from './data-blocks/form/FormActionModel'; +export * from './data-blocks/form/FormFieldModel'; +export * from './data-blocks/form/FormModel'; +export * from './data-blocks/form/QuickEditForm'; +export * from './data-blocks/table/TableActionsColumnModel'; +export * from './data-blocks/table/TableColumnModel'; +export * from './data-blocks/table/TableModel'; +export * from './filter-blocks/form/FilterFormActionModel'; +export * from './filter-blocks/form/FilterFormFieldModel'; +export * from './filter-blocks/form/FilterFormModel'; +export * from './filter-blocks/form/FilterFormResetActionModel'; +export * from './filter-blocks/form/FilterFormSubmitActionModel'; +export * from './other-blocks/html/HtmlBlockModel'; +// diff --git a/packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx b/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx similarity index 89% rename from packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx rename to packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx index 1aea3f6bae..e87e740646 100644 --- a/packages/core/client/src/flow/models/HtmlBlockFlowModel.tsx +++ b/packages/core/client/src/flow/models/other-blocks/html/HtmlBlockModel.tsx @@ -9,7 +9,7 @@ import { Card } from 'antd'; import React, { createRef } from 'react'; -import { BlockFlowModel } from './BlockFlowModel'; +import { BlockModel } from '../../base/BlockModel'; function waitForRefCallback(ref: React.RefObject, cb: (el: T) => void, timeout = 3000) { const start = Date.now(); @@ -21,7 +21,7 @@ function waitForRefCallback(ref: React.RefObject, cb: check(); } -export class HtmlBlockFlowModel extends BlockFlowModel { +export class HtmlBlockModel extends BlockModel { ref = createRef(); render() { return ( @@ -33,11 +33,11 @@ export class HtmlBlockFlowModel extends BlockFlowModel { } } -HtmlBlockFlowModel.define({ +HtmlBlockModel.define({ title: 'HTML', group: 'Content', defaultOptions: { - use: 'HtmlBlockFlowModel', + use: 'HtmlBlockModel', stepParams: { default: { step1: { @@ -49,7 +49,7 @@ HtmlBlockFlowModel.define({ }, }); -HtmlBlockFlowModel.registerFlow({ +HtmlBlockModel.registerFlow({ key: 'default', auto: true, steps: { diff --git a/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx b/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx index e86a069d0c..46dc2bcb6a 100644 --- a/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx +++ b/packages/core/client/src/modules/menu/FlowPageMenuItem.tsx @@ -115,7 +115,7 @@ export const FlowPageMenuItem = () => { export function getFlowPageMenuSchema({ pageSchemaUid, tabSchemaUid, tabSchemaName }) { return { type: 'void', - 'x-component': 'FlowPage', + 'x-component': 'FlowRoute', 'x-uid': pageSchemaUid, }; } 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 4276bc317c..53794e29d8 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,7 +10,7 @@ export enum NocoBaseDesktopRouteType { group = 'group', page = 'page', - flowPage = 'flowPage', + flowRoute = 'flowRoute', link = 'link', tabs = 'tabs', } diff --git a/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx index 1bf8ddfb53..6246a2ae02 100644 --- a/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx +++ b/packages/core/flow-engine/src/components/subModel/AddActionButton.tsx @@ -8,7 +8,7 @@ */ import React, { useMemo } from 'react'; -import { AddSubModelButton } from './AddSubModelButton'; +import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton'; import { FlowModel } from '../../models/flowModel'; import { ModelConstructor } from '../../types'; import { Button } from 'antd'; @@ -32,6 +32,14 @@ interface AddActionButtonProps { * 按钮文本 */ children?: React.ReactNode; + /** + * 过滤Model菜单的函数 + */ + filter?: (blockClass: ModelConstructor, className: string) => boolean; + /** + * 自定义 items(如果提供,将覆盖默认的action菜单) + */ + items?: SubModelItemsType; } /** @@ -51,12 +59,17 @@ export const AddActionButton: React.FC = ({ subModelKey = 'actions', children = , subModelType = 'array', + items, + filter, onModelAdded, }) => { - const items = useMemo(() => { - const blockClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass); + const allActionsItems = useMemo(() => { + const actionClasses = model.flowEngine.filterModelClassByParent(subModelBaseClass); const registeredBlocks = []; - for (const [className, ModelClass] of blockClasses) { + for (const [className, ModelClass] of actionClasses) { + if (filter && !filter(ModelClass, className)) { + continue; + } const item = { key: className, label: ModelClass.meta?.title || className, @@ -76,7 +89,7 @@ export const AddActionButton: React.FC = ({ model={model} subModelKey={subModelKey} subModelType={subModelType} - items={items} + items={items ?? allActionsItems} onModelAdded={onModelAdded} > {children} diff --git a/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx b/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx index c8423da652..12456a0dad 100644 --- a/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx +++ b/packages/core/flow-engine/src/components/subModel/AddBlockButton.tsx @@ -7,11 +7,11 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ +import { Button } from 'antd'; import React, { useMemo } from 'react'; -import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton'; import { FlowModel } from '../../models/flowModel'; import { ModelConstructor } from '../../types'; -import { Button } from 'antd'; +import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton'; import { createBlockItems } from './blockItems'; interface AddBlockButtonProps { @@ -34,13 +34,13 @@ interface AddBlockButtonProps { */ children?: React.ReactNode; /** - * 自定义 items(如果提供,将覆盖默认的数据源选择行为) + * 自定义 items(如果提供,将覆盖默认的区块菜单) */ items?: SubModelItemsType; /** - * 过滤区块类型的函数 + * 过滤Model菜单的函数 */ - filterBlocks?: (blockClass: ModelConstructor, className: string) => boolean; + filter?: (blockClass: ModelConstructor, className: string) => boolean; /** * 追加额外的菜单项到默认菜单中 */ @@ -75,12 +75,12 @@ interface AddBlockButtonProps { */ export const AddBlockButton: React.FC = ({ model, - subModelBaseClass = 'BlockFlowModel', + subModelBaseClass = 'BlockModel', subModelKey = 'blocks', children = , subModelType = 'array', items, - filterBlocks, + filter: filterBlocks, appendItems, onModelAdded, }) => { diff --git a/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx index e33a9ad053..1968002d78 100644 --- a/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx +++ b/packages/core/flow-engine/src/components/subModel/AddFieldButton.tsx @@ -42,6 +42,10 @@ export interface AddFieldButtonProps { * 显示的UI组件 */ children?: React.ReactNode; + /** + * 自定义 items(如果提供,将覆盖默认的字段菜单) + */ + items?: SubModelItemsType; } /** @@ -63,6 +67,7 @@ export const AddFieldButton: React.FC = ({ subModelType = 'array', collection, buildCreateModelOptions, + items, appendItems, onModelAdded, }) => { @@ -117,7 +122,7 @@ export const AddFieldButton: React.FC = ({ }; }, [model, subModelBaseClass, fields, buildCreateModelOptions]); - const items = useMemo(() => { + const fieldItems = useMemo(() => { return mergeSubModelItems([buildFieldItems, appendItems], { addDividers: true }); }, [buildFieldItems, appendItems]); @@ -126,7 +131,7 @@ export const AddFieldButton: React.FC = ({ model={model} subModelKey={subModelKey} subModelType={subModelType} - items={items} + items={items ?? fieldItems} onModelAdded={onModelAdded} > {children} diff --git a/packages/core/flow-engine/src/flowEngine.ts b/packages/core/flow-engine/src/flowEngine.ts index 39e66ae6c9..f2e7352055 100644 --- a/packages/core/flow-engine/src/flowEngine.ts +++ b/packages/core/flow-engine/src/flowEngine.ts @@ -184,6 +184,10 @@ export class FlowEngine { return this.modelInstances.get(uid) as T | undefined; } + forEachModel(callback: (model: T) => void): void { + this.modelInstances.forEach(callback); + } + /** * 移除一个本地模型实例。 * @param {string} uid 要销毁的 Model 实例的唯一标识符。 diff --git a/packages/core/flow-engine/src/models/flowModel.tsx b/packages/core/flow-engine/src/models/flowModel.tsx index 9fc633133d..b0aa0a1edf 100644 --- a/packages/core/flow-engine/src/models/flowModel.tsx +++ b/packages/core/flow-engine/src/models/flowModel.tsx @@ -58,7 +58,10 @@ export class FlowModel> = new Map(); - // model 树的共享运行上下文 + + /** + * model 树的共享运行上下文 + */ private _sharedContext: Record = {}; constructor(options: FlowModelOptions) { @@ -95,6 +98,10 @@ export class FlowModel(subKey: K, extra?: Record) { + async applySubModelsAutoFlows( + subKey: K, + extra?: Record, + shared?: Record, + ) { await Promise.all( this.mapSubModels(subKey, async (column) => { + column.setSharedContext(shared); await column.applyAutoFlows(extra); }), ); @@ -726,11 +738,21 @@ export class FlowModel) { this._sharedContext = ctx; } public getSharedContext() { + if (this.async || !this.parent) { + return this._sharedContext; + } return { ...this.parent?.getSharedContext(), ...this._sharedContext, // 当前实例的 context 优先级最高 diff --git a/packages/core/flow-engine/src/models/forkFlowModel.ts b/packages/core/flow-engine/src/models/forkFlowModel.ts index 7c51dd2602..6942c29759 100644 --- a/packages/core/flow-engine/src/models/forkFlowModel.ts +++ b/packages/core/flow-engine/src/models/forkFlowModel.ts @@ -7,191 +7,216 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ - import { action, define, observable } from '@formily/reactive'; - import type { IModelComponentProps } from '../types'; - import { FlowModel } from './flowModel'; - - /** - * ForkFlowModel 作为 FlowModel 的轻量代理实例: - * - 共享 master(原始 FlowModel)上的所有业务数据与方法 - * - 仅在 props 层面拥有本地覆盖(localProps),其余字段全部透传到 master - * - 透传的函数中 this 指向 fork 实例,而非 master,确保正确的上下文 - * - 使用 Object.create 创建临时上下文,确保 this.constructor 指向正确的类(避免异步竞态条件) - * - setter 方法中的 this 也指向 fork 实例,保持一致的上下文行为 - * - 不会被注册到 FlowEngine.modelInstances 中,保持 uid → master 唯一性假设 - */ - export class ForkFlowModel { - /** 与 master 相同的 UID,用于日志调试 */ - public readonly uid: string; - /** 调试标识,便于在日志或断言中快速识别 */ - public readonly isFork = true; - - /** 本地覆盖的 props,fork 层面的 UI/状态 */ - public localProps: IModelComponentProps; - - /** master 引用 */ - private master: TMaster; - - /** 是否已被释放 */ - private disposed = false; - - /** fork 在 master.forks 中的索引 */ - public readonly forkId: number; - - constructor(master: TMaster, initialProps: IModelComponentProps = {}, forkId = 0) { - this.master = master; - this.uid = master.uid; - this.localProps = { ...initialProps }; - this.forkId = forkId; - - define(this, { - localProps: observable, - setProps: action, - }); - - // 返回代理对象,实现自动透传 - return new Proxy(this, { - get: (target: any, prop: PropertyKey, receiver: any) => { - // disposed check - if (prop === 'disposed') return target.disposed; - - // 特殊处理 constructor,应该返回 master 的 constructor - if (prop === 'constructor') { - return target.master.constructor; - } - if (prop === 'props') { - // 对 props 做合并返回 - return { ...target.master.getProps(), ...target.localProps }; - } - // fork 自身属性 / 方法优先 - if (prop in target) { - return Reflect.get(target, prop, receiver); - } - - // 默认取 master 上的值 - const value = (target.master as any)[prop]; - - // 如果是函数,需要绑定到 fork 实例,让 this 指向 fork - // 使用闭包捕获正确的 constructor,避免异步方法中的竞态条件 - if (typeof value === 'function') { - const masterConstructor = target.master.constructor; - return function (this: any, ...args: any[]) { - // 创建一个临时的 this 对象,包含正确的 constructor - const contextThis = Object.create(this); - Object.defineProperty(contextThis, 'constructor', { - value: masterConstructor, - configurable: true, - enumerable: false, - writable: false, - }); - - return value.apply(contextThis, args); - }.bind(receiver); - } - return value; - }, - set: (target: any, prop: PropertyKey, value: any, receiver: any) => { - if (prop === 'props') { - return true; - } - - // 如果 fork 自带字段,则写到自身(例如 localProps) - if (prop in target) { - return Reflect.set(target, prop, value, receiver); - } - - // 其余写入 master,但需要确保 setter 中的 this 指向 fork - // 检查 master 上是否有对应的 setter - const descriptor = this.getPropertyDescriptor(target.master, prop); - if (descriptor && descriptor.set) { - // 如果有 setter,直接用 receiver(fork 实例)作为 this 调用 - // 这样 setter 中的 this 就指向 fork,可以正确调用 fork 的方法 - descriptor.set.call(receiver, value); - return true; - } else { - // 没有 setter,直接赋值到 master - (target.master as any)[prop] = value; - return true; - } - }, - }); - } - - /** - * 获取对象及其原型链上的属性描述符 - */ - private getPropertyDescriptor(obj: any, prop: PropertyKey): PropertyDescriptor | undefined { - let current = obj; - while (current) { - const descriptor = Object.getOwnPropertyDescriptor(current, prop); - if (descriptor) { - return descriptor; - } - current = Object.getPrototypeOf(current); - } - return undefined; - } - - /** - * 修改局部 props,仅影响当前 fork - */ - setProps(key: string | IModelComponentProps, value?: any): void { - if (this.disposed) return; - - if (typeof key === 'string') { - this.localProps[key] = value; - } else { - this.localProps = { ...this.localProps, ...key }; - } - } - - /** - * render 依旧使用 master 的方法,但合并后的 props 需要透传 - */ - render() { - if (this.disposed) return null; - // 将 master.render 以 fork 作为 this 调用,使其读取到合并后的 props - const mergedProps = { ...this.master.getProps(), ...this.localProps }; - // 临时替换 this.props - const originalProps = (this as any).props; - (this as any).props = mergedProps; - try { - return (this.master.render as any).call(this); - } finally { - (this as any).props = originalProps; - } - } - - /** - * 释放 fork:从 master.forks 中移除自身并断开引用 - */ - dispose() { - if (this.disposed) return; - this.disposed = true; - if (this.master && (this.master as any).forks) { - (this.master as any).forks.delete(this as any); - } - // 从 master 的 forkCache 中移除自己 - if (this.master && (this.master as any).forkCache) { - const forkCache = (this.master as any).forkCache; - for (const [key, fork] of forkCache.entries()) { - if (fork === this) { - forkCache.delete(key); - break; - } - } - } - // @ts-ignore - this.master = null; - } - - /** - * 获取合并后的 props(master + localProps,local 优先) - */ - getProps(): IModelComponentProps { - return { ...this.master.getProps(), ...this.localProps }; - } - } - - // 类型断言:让 ForkFlowModel 可以被当作 FlowModel 使用 - export interface ForkFlowModel extends FlowModel {} \ No newline at end of file +import { action, define, observable } from '@formily/reactive'; +import type { IModelComponentProps } from '../types'; +import { FlowModel } from './flowModel'; + +/** + * ForkFlowModel 作为 FlowModel 的轻量代理实例: + * - 共享 master(原始 FlowModel)上的所有业务数据与方法 + * - 仅在 props 层面拥有本地覆盖(localProps),其余字段全部透传到 master + * - 透传的函数中 this 指向 fork 实例,而非 master,确保正确的上下文 + * - 使用 Object.create 创建临时上下文,确保 this.constructor 指向正确的类(避免异步竞态条件) + * - setter 方法中的 this 也指向 fork 实例,保持一致的上下文行为 + * - 不会被注册到 FlowEngine.modelInstances 中,保持 uid → master 唯一性假设 + */ +export class ForkFlowModel { + /** 与 master 相同的 UID,用于日志调试 */ + public readonly uid: string; + /** 调试标识,便于在日志或断言中快速识别 */ + public readonly isFork = true; + + /** 本地覆盖的 props,fork 层面的 UI/状态 */ + public localProps: IModelComponentProps; + + /** master 引用 */ + private master: TMaster; + + /** 是否已被释放 */ + private disposed = false; + + /** fork 在 master.forks 中的索引 */ + public readonly forkId: number; + + /** 用于共享上下文的对象,存储跨 fork 的共享数据 */ + // private _sharedContext: Record = {}; + + constructor(master: TMaster, initialProps: IModelComponentProps = {}, forkId = 0) { + this.master = master; + this.uid = master.uid; + this.localProps = { ...initialProps }; + this.forkId = forkId; + + define(this, { + localProps: observable, + setProps: action, + }); + + // 返回代理对象,实现自动透传 + return new Proxy(this, { + get: (target: any, prop: PropertyKey, receiver: any) => { + // disposed check + if (prop === 'disposed') return target.disposed; + + // 特殊处理 constructor,应该返回 master 的 constructor + if (prop === 'constructor') { + return target.master.constructor; + } + if (prop === 'props') { + // 对 props 做合并返回 + return { ...target.master.getProps(), ...target.localProps }; + } + // fork 自身属性 / 方法优先 + if (prop in target) { + return Reflect.get(target, prop, receiver); + } + + // 默认取 master 上的值 + const value = (target.master as any)[prop]; + + // 如果是函数,需要绑定到 fork 实例,让 this 指向 fork + // 使用闭包捕获正确的 constructor,避免异步方法中的竞态条件 + if (typeof value === 'function') { + const masterConstructor = target.master.constructor; + return function (this: any, ...args: any[]) { + // 创建一个临时的 this 对象,包含正确的 constructor + const contextThis = Object.create(this); + Object.defineProperty(contextThis, 'constructor', { + value: masterConstructor, + configurable: true, + enumerable: false, + writable: false, + }); + + return value.apply(contextThis, args); + }.bind(receiver); + } + return value; + }, + set: (target: any, prop: PropertyKey, value: any, receiver: any) => { + if (prop === 'props') { + return true; + } + + // 如果 fork 自带字段,则写到自身(例如 localProps) + if (prop in target) { + return Reflect.set(target, prop, value, receiver); + } + + // 其余写入 master,但需要确保 setter 中的 this 指向 fork + // 检查 master 上是否有对应的 setter + const descriptor = this.getPropertyDescriptor(target.master, prop); + if (descriptor && descriptor.set) { + // 如果有 setter,直接用 receiver(fork 实例)作为 this 调用 + // 这样 setter 中的 this 就指向 fork,可以正确调用 fork 的方法 + descriptor.set.call(receiver, value); + return true; + } else { + // 没有 setter,直接赋值到 master + (target.master as any)[prop] = value; + return true; + } + }, + }); + } + + public setSharedContext(ctx: Record) { + this['_sharedContext'] = ctx; + } + + public getSharedContext() { + if (this.async || !this.parent) { + return this['_sharedContext'] || {}; + } + return { + ...this.parent?.getSharedContext(), + ...this['_sharedContext'], // 当前实例的 context 优先级最高 + }; + } + + get ctx() { + return { + globals: this.flowEngine.getContext(), + shared: this.getSharedContext(), + }; + } + + /** + * 获取对象及其原型链上的属性描述符 + */ + private getPropertyDescriptor(obj: any, prop: PropertyKey): PropertyDescriptor | undefined { + let current = obj; + while (current) { + const descriptor = Object.getOwnPropertyDescriptor(current, prop); + if (descriptor) { + return descriptor; + } + current = Object.getPrototypeOf(current); + } + return undefined; + } + + /** + * 修改局部 props,仅影响当前 fork + */ + setProps(key: string | IModelComponentProps, value?: any): void { + if (this.disposed) return; + + if (typeof key === 'string') { + this.localProps[key] = value; + } else { + this.localProps = { ...this.localProps, ...key }; + } + } + + /** + * render 依旧使用 master 的方法,但合并后的 props 需要透传 + */ + render() { + if (this.disposed) return null; + // 将 master.render 以 fork 作为 this 调用,使其读取到合并后的 props + const mergedProps = { ...this.master.getProps(), ...this.localProps }; + // 临时替换 this.props + const originalProps = (this as any).props; + (this as any).props = mergedProps; + try { + return (this.master.render as any).call(this); + } finally { + (this as any).props = originalProps; + } + } + + /** + * 释放 fork:从 master.forks 中移除自身并断开引用 + */ + dispose() { + if (this.disposed) return; + this.disposed = true; + if (this.master && (this.master as any).forks) { + (this.master as any).forks.delete(this as any); + } + // 从 master 的 forkCache 中移除自己 + if (this.master && (this.master as any).forkCache) { + const forkCache = (this.master as any).forkCache; + for (const [key, fork] of forkCache.entries()) { + if (fork === this) { + forkCache.delete(key); + break; + } + } + } + // @ts-ignore + this.master = null; + } + + /** + * 获取合并后的 props(master + localProps,local 优先) + */ + getProps(): IModelComponentProps { + return { ...this.master.getProps(), ...this.localProps }; + } +} + +// 类型断言:让 ForkFlowModel 可以被当作 FlowModel 使用 +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ForkFlowModel extends FlowModel {} diff --git a/packages/core/flow-engine/src/resources/baseRecordResource.ts b/packages/core/flow-engine/src/resources/baseRecordResource.ts index 0f174408f0..7309b2959d 100644 --- a/packages/core/flow-engine/src/resources/baseRecordResource.ts +++ b/packages/core/flow-engine/src/resources/baseRecordResource.ts @@ -31,6 +31,8 @@ export abstract class BaseRecordResource extends APIResource headers: {} as Record, }; + protected filterGroups = new Map(); + protected splitValue(value: string | string[]): string[] { if (typeof value === 'string') { return value.split(',').map((item) => item.trim()); @@ -100,11 +102,27 @@ export abstract class BaseRecordResource extends APIResource } setFilter(filter: Record) { - return this.addRequestParameter('filter', filter); + return this.addRequestParameter('filter', JSON.stringify(filter)); } getFilter(): Record { - return this.request.params.filter; + return { + $and: [...this.filterGroups.values()].filter(Boolean), + }; + } + + resetFilter() { + this.setFilter(this.getFilter()); + } + + addFilterGroup(key: string, filter) { + this.filterGroups.set(key, filter); + this.resetFilter(); + } + + removeFilterGroup(key: string) { + this.filterGroups.delete(key); + this.resetFilter(); } setAppends(appends: string[]) { diff --git a/packages/core/flow-engine/src/types.ts b/packages/core/flow-engine/src/types.ts index f9b59f1c8e..f3e0929981 100644 --- a/packages/core/flow-engine/src/types.ts +++ b/packages/core/flow-engine/src/types.ts @@ -285,6 +285,7 @@ export interface DefaultStructure { */ export interface FlowModelOptions { uid: string; + async?: boolean; // 是否异步加载模型 props?: IModelComponentProps; stepParams?: Record; subModels?: Structure['subModels'];