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"
+ >
+
+
+ );
+ }
+}
diff --git a/packages/core/client/src/flow/models/ViewActionModel.tsx b/packages/core/client/src/flow/models/actions/LinkActionModel.tsx
similarity index 55%
rename from packages/core/client/src/flow/models/ViewActionModel.tsx
rename to packages/core/client/src/flow/models/actions/LinkActionModel.tsx
index c047ae0227..884b0c7951 100644
--- a/packages/core/client/src/flow/models/ViewActionModel.tsx
+++ b/packages/core/client/src/flow/models/actions/LinkActionModel.tsx
@@ -7,24 +7,24 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import type { ButtonProps } from 'antd/es/button';
-import { ActionModel } from './ActionModel';
-import { openModeAction } from '../actions/openModeAction';
+import type { ButtonProps } from 'antd';
+import { ActionModel } from '../base/ActionModel';
-export class ViewActionModel extends ActionModel {
+export class LinkActionModel extends ActionModel {
defaultProps: ButtonProps = {
type: 'link',
- title: 'View',
+ children: 'Link',
};
}
-ViewActionModel.registerFlow({
- key: 'handleClick',
- title: '点击事件',
+LinkActionModel.registerFlow({
+ key: 'event1',
on: {
eventName: 'click',
},
steps: {
- open: openModeAction,
+ step1: {
+ handler(ctx, params) {},
+ },
},
});
diff --git a/packages/core/client/src/flow/models/PopupActionModel.tsx b/packages/core/client/src/flow/models/actions/PopupActionModel.tsx
similarity index 84%
rename from packages/core/client/src/flow/models/PopupActionModel.tsx
rename to packages/core/client/src/flow/models/actions/PopupActionModel.tsx
index 826e926b0e..cddaae0f44 100644
--- a/packages/core/client/src/flow/models/PopupActionModel.tsx
+++ b/packages/core/client/src/flow/models/actions/PopupActionModel.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 PopupActionModel extends ActionModel {
defaultProps: ButtonProps = {
diff --git a/packages/core/client/src/flow/models/actions/RefreshActionModel.tsx b/packages/core/client/src/flow/models/actions/RefreshActionModel.tsx
new file mode 100644
index 0000000000..6c7aad7fa1
--- /dev/null
+++ b/packages/core/client/src/flow/models/actions/RefreshActionModel.tsx
@@ -0,0 +1,36 @@
+/**
+ * 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 { ActionModel } from '../base/ActionModel';
+
+export class RefreshActionModel extends ActionModel {
+ defaultProps: ButtonProps = {
+ children: 'Refresh',
+ };
+}
+
+RefreshActionModel.registerFlow({
+ key: 'event1',
+ on: {
+ eventName: 'click',
+ },
+ steps: {
+ step1: {
+ async handler(ctx, params) {
+ if (!ctx.shared?.currentBlockModel?.resource) {
+ ctx.globals.message.error('No resource selected for refresh.');
+ return;
+ }
+ const currentBlockModel = ctx.shared.currentBlockModel;
+ await currentBlockModel.resource.refresh();
+ },
+ },
+ },
+});
diff --git a/packages/core/client/src/flow/models/UpdateRecordActionModel.tsx b/packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx
similarity index 70%
rename from packages/core/client/src/flow/models/UpdateRecordActionModel.tsx
rename to packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx
index f3b0c58c43..ef020865f9 100644
--- a/packages/core/client/src/flow/models/UpdateRecordActionModel.tsx
+++ b/packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx
@@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import type { ButtonProps } from 'antd/es/button';
-import { ActionModel } from './ActionModel';
-import { secondaryConfirmationAction } from '../actions/secondaryConfirmationAction';
-import { refreshOnCompleteAction } from '../actions/refreshOnCompleteAction';
-import { afterSuccessAction } from '../actions/afterSuccessAction';
+import type { ButtonProps } from 'antd';
+import { afterSuccessAction } from '../../actions/afterSuccessAction';
+import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
+import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
+import { ActionModel } from '../base/ActionModel';
export class UpdateRecordActionModel extends ActionModel {
defaultProps: ButtonProps = {
diff --git a/packages/core/client/src/flow/models/AddNewActionModel.tsx b/packages/core/client/src/flow/models/actions/ViewActionModel.tsx
similarity index 55%
rename from packages/core/client/src/flow/models/AddNewActionModel.tsx
rename to packages/core/client/src/flow/models/actions/ViewActionModel.tsx
index 7eb35eff5d..5366bb211b 100644
--- a/packages/core/client/src/flow/models/AddNewActionModel.tsx
+++ b/packages/core/client/src/flow/models/actions/ViewActionModel.tsx
@@ -7,16 +7,19 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
-import type { ButtonType } from 'antd/es/button';
+import { ButtonProps } from 'antd';
import React from 'react';
-import { FlowPageComponent } from '../FlowPage';
-import { ActionModel } from './ActionModel';
+import { FlowPage } from '../../FlowPage';
+import { ActionModel } from '../base/ActionModel';
-export class AddNewActionModel extends ActionModel {
- title = 'Add new';
+export class ViewActionModel extends ActionModel {
+ defaultProps: ButtonProps = {
+ children: 'View',
+ type: 'link',
+ };
}
-AddNewActionModel.registerFlow({
+ViewActionModel.registerFlow({
key: 'event1',
on: {
eventName: 'click',
@@ -30,13 +33,20 @@ AddNewActionModel.registerFlow({
function DrawerContent() {
return (
-
+
);
}
currentDrawer = ctx.globals.drawer.open({
- title: '命令式 Drawer',
+ // title: '命令式 Drawer',
width: 800,
content: ,
});
diff --git a/packages/core/client/src/flow/models/ActionModel.tsx b/packages/core/client/src/flow/models/base/ActionModel.tsx
similarity index 50%
rename from packages/core/client/src/flow/models/ActionModel.tsx
rename to packages/core/client/src/flow/models/base/ActionModel.tsx
index 6bb46a86db..16e93ae33e 100644
--- a/packages/core/client/src/flow/models/ActionModel.tsx
+++ b/packages/core/client/src/flow/models/base/ActionModel.tsx
@@ -11,62 +11,52 @@ import { FlowModel } from '@nocobase/flow-engine';
import { Button } from 'antd';
import type { ButtonProps } from 'antd/es/button';
import React from 'react';
-import IconPicker from '../../schema-component/antd/icon-picker/IconPicker';
-import { Icon } from '../../icon/Icon';
export class ActionModel extends FlowModel {
- declare props: ButtonProps;
-
defaultProps: ButtonProps = {
type: 'default',
- title: 'Action',
+ children: 'Action',
};
-
render() {
- const props = { ...this.defaultProps, ...this.props };
- const icon = ;
-
return (
-
+
-
+ */}
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 = Configure actions,
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 = Add block,
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'];