diff --git a/packages/core/client/src/flow/actions/confirm.tsx b/packages/core/client/src/flow/actions/confirm.tsx index 75bdc8268f4..aa7fdb60690 100644 --- a/packages/core/client/src/flow/actions/confirm.tsx +++ b/packages/core/client/src/flow/actions/confirm.tsx @@ -43,8 +43,8 @@ export const confirm = defineAction({ }, defaultParams: { enable: true, - title: 'Please Confirm', - content: 'Are you sure you want to proceed with this action?', + title: escapeT('Please Confirm'), + content: escapeT('Are you sure you want to perform the action?'), }, async handler(ctx, params) { if (params.enable) { diff --git a/packages/core/client/src/flow/models/actions/UpdateActionModel.tsx b/packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx similarity index 80% rename from packages/core/client/src/flow/models/actions/UpdateActionModel.tsx rename to packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx index ff991f59c5f..acb21e11dc2 100644 --- a/packages/core/client/src/flow/models/actions/UpdateActionModel.tsx +++ b/packages/core/client/src/flow/models/actions/UpdateRecordActionModel.tsx @@ -10,8 +10,10 @@ import { escapeT, FlowModelRenderer, useFlowEngine, useFlowSettingsContext } from '@nocobase/flow-engine'; import { Alert, ButtonProps } from 'antd'; import React, { useEffect, useRef } from 'react'; -import { ActionModel, ActionSceneEnum, CollectionActionModel, RecordActionModel } from '../base'; -import { AssignFormModel } from '../blocks'; +import { ActionModel, ActionSceneEnum } from '../base/ActionModel'; +import { CollectionActionModel } from '../base/CollectionActionModel'; +import { RecordActionModel } from '../base/RecordActionModel'; +import { AssignFormModel } from '../blocks/assign-form/AssignFormModel'; // import { RemoteFlowModelRenderer } from '../../FlowPage'; const SETTINGS_FLOW_KEY = 'assignSettings'; @@ -63,22 +65,26 @@ function AssignFieldsEditor() { if (isBulk && (formModel as any)?.context?.defineProperty) { formModel.context.defineProperty('record', { get: () => undefined }); } - const grid = (formModel as any)?.subModels?.grid; - const items = grid?.subModels?.items || []; - for (const it of items) { - const saved = - typeof it?.getStepParams === 'function' ? it.getStepParams('fieldSettings', 'assignValue')?.value : undefined; - if (typeof saved !== 'undefined') { - (it as any).assignValue = saved; - } - } initializedRef.current = true; }, [action, blockModel?.collection, formModel]); return formModel ? : null; } -export class UpdateActionModel extends ActionModel<{ +function Info() { + const ctx = useFlowSettingsContext(); + return ( + + ); +} + +export class UpdateRecordActionModel extends ActionModel<{ subModels: { assignForm: AssignFormModel; }; @@ -88,7 +94,7 @@ export class UpdateActionModel extends ActionModel<{ assignFormUid?: string; defaultProps: ButtonProps = { - title: escapeT('Update'), + title: escapeT('Update record'), type: 'link', }; @@ -97,8 +103,8 @@ export class UpdateActionModel extends ActionModel<{ } } -UpdateActionModel.define({ - label: escapeT('Update'), +UpdateRecordActionModel.define({ + label: escapeT('Update record'), // 使用函数型 createModelOptions,从父级上下文提取资源信息,直接注入到子模型的 resourceSettings.init createModelOptions: (ctx) => { const dsKey = ctx.collection.dataSourceKey; @@ -116,7 +122,7 @@ UpdateActionModel.define({ }, }); -UpdateActionModel.registerFlow({ +UpdateRecordActionModel.registerFlow({ key: SETTINGS_FLOW_KEY, title: escapeT('Action settings'), // 配置流仅用于收集参数,避免作为自动流程执行 @@ -138,9 +144,7 @@ UpdateActionModel.registerFlow({ return { tip: { 'x-decorator': 'FormItem', - 'x-component': () => ( - - ), + 'x-component': () => , }, editor: { 'x-decorator': 'FormItem', @@ -149,31 +153,20 @@ UpdateActionModel.registerFlow({ }; }, async beforeParamsSave(ctx) { - const m = ctx.model as UpdateActionModel; - let form: AssignFormModel = m?.assignFormUid && ctx.engine.getModel?.(m.assignFormUid); - if (!form && ctx.engine) { - form = (await ctx.engine.loadModel({ - uid: m.assignFormUid || undefined, - parentId: ctx.model.uid, - subKey: 'assignForm', - })) as any; - } + const m = ctx.model as UpdateRecordActionModel; + // 跨视图栈按 uid 定位到设置面板中的真实 AssignForm 实例 + const form: AssignFormModel = m?.assignFormUid && ctx.engine.getModel?.(m.assignFormUid, true); if (!form) return; const assignedValues = form?.getAssignedValues?.() || {}; const grid = form?.subModels?.grid; const items = grid?.subModels?.items || []; - for (const it of items) { - if (typeof it?.setStepParams === 'function') { - it.setStepParams('fieldSettings', 'assignValue', { value: it.assignValue }); - } - } ctx.model.setStepParams(SETTINGS_FLOW_KEY, 'assignFieldValues', { assignedValues }); }, }, }, }); -UpdateActionModel.registerFlow({ +UpdateRecordActionModel.registerFlow({ key: 'apply', on: 'click', steps: { diff --git a/packages/core/client/src/flow/models/actions/__tests__/AssignFormRefill.test.ts b/packages/core/client/src/flow/models/actions/__tests__/AssignFormRefill.test.ts new file mode 100644 index 00000000000..68edac3d049 --- /dev/null +++ b/packages/core/client/src/flow/models/actions/__tests__/AssignFormRefill.test.ts @@ -0,0 +1,55 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { describe, it, expect } from 'vitest'; +import { FlowEngine, FlowModel } from '@nocobase/flow-engine'; +import { UpdateRecordActionModel } from '../UpdateRecordActionModel'; + +/** + * 精简版 AssignFormModel(仅用于单测): + * - 避免依赖复杂上下文;专注验证 beforeParamsSave 的聚合入参写入逻辑。 + */ +class TestAssignFormModel extends FlowModel { + private _values: Record = {}; + setAssignedValues(v: Record) { + this._values = v || {}; + } + getAssignedValues(): Record { + return this._values || {}; + } +} + +describe('AssignForm value refill and save (beforeParamsSave)', () => { + it('UpdateRecordActionModel: saves non-empty assignedValues from AssignForm', async () => { + const root = new FlowEngine(); + + // 仅 root 引擎:真实场景视图作用域引擎由弹窗创建,此处不需 link 模拟 + root.registerModels({ UpdateRecordActionModel, AssignFormModel: TestAssignFormModel }); + + const action = root.createModel({ use: 'UpdateRecordActionModel', uid: 'act-u' }); + + const form = root.createModel({ + use: 'AssignFormModel', + uid: 'form-u', + parentId: action.uid, + subKey: 'assignForm', + }); + form.setAssignedValues({ nickname: 'Alice', score: 99 }); + (action as any).assignFormUid = form.uid; + + const flow = action.getFlow('assignSettings') as any; + const step = flow?.steps?.assignFieldValues; + expect(step?.beforeParamsSave).toBeTypeOf('function'); + + await step.beforeParamsSave({ engine: root, model: action }); + + const saved = action.getStepParams('assignSettings', 'assignFieldValues'); + expect(saved?.assignedValues).toEqual({ nickname: 'Alice', score: 99 }); + }); +}); diff --git a/packages/core/client/src/flow/models/actions/index.ts b/packages/core/client/src/flow/models/actions/index.ts index 9541309485a..c93cb1ad37b 100644 --- a/packages/core/client/src/flow/models/actions/index.ts +++ b/packages/core/client/src/flow/models/actions/index.ts @@ -16,6 +16,6 @@ export * from './JSCollectionActionModel'; export * from './JSRecordActionModel'; export * from './PopupCollectionActionModel'; export * from './RefreshActionModel'; -export * from './UpdateActionModel'; +export * from './UpdateRecordActionModel'; export * from './ViewActionModel'; // diff --git a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx index 37690f32875..93655a87981 100644 --- a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +++ b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx @@ -85,7 +85,17 @@ export class AssignFormGridModel extends FormGridModel { addOrEnsureItem(fieldName: string, value?: any) { const collection = (this.context as any)?.collection; const items = this.subModels?.items || []; - const existing = items.find((m) => m?.context.fieldPath === fieldName); + // AssignFormItemModel 并非 CollectionFieldModel,不存在 context.fieldPath, + // 需要依据步骤参数中声明的 fieldSettings.init.fieldPath 来判断是否已存在同名条目 + const existing = items.find((m) => { + try { + const init = + typeof (m as any)?.getStepParams === 'function' ? (m as any).getStepParams('fieldSettings', 'init') : null; + return init?.fieldPath === fieldName; + } catch (_e) { + return false; + } + }); if (existing) { // 更新已有项的当前值,便于重新打开时回填 if (typeof (existing as any)?.setStepParams === 'function') { diff --git a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx index b7fbf5c8703..04184ae721f 100644 --- a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +++ b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx @@ -133,7 +133,7 @@ export class AssignFormItemModel extends FormItemModel { }); if (!created) return; - // 将集合/数据源/字段/区块/资源注入临时根,保证字段组件行为一致 + // 将集合/数据源/字段/区块/资源/表单注入临时根,保证字段组件行为一致 created.context?.defineProperty?.('collection', { value: collection }); const ds = ctx?.dataSource; if (ds) created.context?.defineProperty?.('dataSource', { value: ds }); @@ -141,6 +141,10 @@ export class AssignFormItemModel extends FormItemModel { if (cf2) created.context?.defineProperty?.('collectionField', { value: cf2 }); const block = ctx?.blockModel; if (block) created.context?.defineProperty?.('blockModel', { value: block }); + const parentForm = ctx.form; + if (parentForm) { + created.context?.defineProperty?.('form', { value: parentForm }); + } if (created.context) { Object.defineProperty(created.context, 'resource', { configurable: true, @@ -267,6 +271,7 @@ export class AssignFormItemModel extends FormItemModel { }} metaTree={mergedMetaTree} converters={converters} + clearValue={''} /> ); diff --git a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormModel.tsx b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormModel.tsx index 8fd89d1efd5..78eca36a381 100644 --- a/packages/core/client/src/flow/models/blocks/assign-form/AssignFormModel.tsx +++ b/packages/core/client/src/flow/models/blocks/assign-form/AssignFormModel.tsx @@ -7,15 +7,52 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { FlowModelRenderer, SingleRecordResource, createCollectionContextMeta, escapeT } from '@nocobase/flow-engine'; +import { + FlowModelRenderer, + SingleRecordResource, + createCollectionContextMeta, + escapeT, + createCurrentRecordMetaFactory, + type PropertyMetaFactory, +} from '@nocobase/flow-engine'; import React from 'react'; +import { Form } from 'antd'; import { FormBlockModel, FormComponent } from '../form/FormBlockModel'; +import { FilterManager } from '../filter-manager/FilterManager'; /** * 赋值配置表单 */ // 使用范型标注 subModels.grid 的类型,提升类型提示与可读性 export class AssignFormModel extends FormBlockModel<{ subModels: { grid: any } }> { + // 覆盖:不注入 formValues(当前表单)到上下文元数据,避免变量树出现“当前表单” + useHooksBeforeRender() { + // 仍需提供 antd form 实例以保证 FormComponent 正常渲染 + // 但不定义 `formValues`,从而不暴露“当前表单”变量 + // eslint-disable-next-line react-hooks/rules-of-hooks + const [form] = Form.useForm(); + this.context.defineProperty('form', { get: () => form }); + } + + onInit(options: any) { + super.onInit(options); + // 默认:补充 meta,使“当前记录”变量可用 + const recordMeta: PropertyMetaFactory = createCurrentRecordMetaFactory(this.context, () => this.collection); + this.context.defineProperty('record', { + get: () => this.getCurrentRecord?.(), + cache: false, + resolveOnServer: true, + meta: recordMeta, + }); + // 该模型常在“配置面板”场景独立使用,不一定挂在 BlockGridModel 下, + // 因此需要在本地补齐 filterManager,避免基础刷新流访问时出现空指针。 + this.context.defineProperty('filterManager', { + once: true, + get: () => new FilterManager(this, options?.filterManager), + }); + // 配置表单不需要在挂载时触发资源刷新,避免无意义的网络交互 + this.isManualRefresh = true; + } createResource(ctx: any, params: any) { const resource = this.context.createResource(SingleRecordResource); // 行为与 CreateFormModel 一致:视为新记录,避免额外 GET diff --git a/packages/core/client/src/flow/models/blocks/form/CreateFormModel.tsx b/packages/core/client/src/flow/models/blocks/form/CreateFormModel.tsx index fdf3c65c473..f1344613d77 100644 --- a/packages/core/client/src/flow/models/blocks/form/CreateFormModel.tsx +++ b/packages/core/client/src/flow/models/blocks/form/CreateFormModel.tsx @@ -18,7 +18,7 @@ import { } from '@nocobase/flow-engine'; import { Space } from 'antd'; import React from 'react'; -import { BlockSceneEnum } from '../../base'; +import { BlockSceneEnum } from '../../base/BlockModel'; import { FormBlockModel, FormComponent } from './FormBlockModel'; // CreateFormModel - 专门用于新增记录 diff --git a/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx b/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx index 032b9587242..8a1bbea321b 100644 --- a/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx +++ b/packages/core/client/src/flow/models/blocks/form/FormBlockModel.tsx @@ -19,7 +19,8 @@ import { import { Form, FormInstance } from 'antd'; import { omit } from 'lodash'; import React from 'react'; -import { BlockGridModel, CollectionBlockModel } from '../../base'; +import { BlockGridModel } from '../../base/BlockGridModel'; +import { CollectionBlockModel } from '../../base/CollectionBlockModel'; import { FormActionModel } from './FormActionModel'; import { FormGridModel } from './FormGridModel'; import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder'; diff --git a/packages/core/client/src/flow/models/fields/VariableFieldFormModel.tsx b/packages/core/client/src/flow/models/fields/VariableFieldFormModel.tsx index 37f7f4e47f5..dcaa6936461 100644 --- a/packages/core/client/src/flow/models/fields/VariableFieldFormModel.tsx +++ b/packages/core/client/src/flow/models/fields/VariableFieldFormModel.tsx @@ -47,6 +47,16 @@ export class VariableFieldFormModel extends FlowModel { {this.mapSubModels('fields', (field) => { + // 确保字段模型具备稳定的 id/name,便于依赖路径的组件(如公式字段)正确解析 + const init = field?.getStepParams?.('fieldSettings', 'init') || {}; + const fp = init?.fieldPath as string | undefined; + if (fp) { + const namePath = fp.includes('.') ? fp.split('.') : [fp]; + const toSet: any = {}; + if (!field?.props?.id) toSet.id = namePath; + if (!field?.props?.name) toSet.name = namePath; + if (Object.keys(toSet).length) field?.setProps?.(toSet); + } return ; })} diff --git a/packages/core/client/src/locale/en-US.json b/packages/core/client/src/locale/en-US.json index 0e9bc79e3ca..475f92eab10 100644 --- a/packages/core/client/src/locale/en-US.json +++ b/packages/core/client/src/locale/en-US.json @@ -992,5 +992,6 @@ "No form available for reset.": "No form available for reset.", "No form available for submission.": "No form available for submission.", "Collapse button": "Collapse", - "Expand button": "Expand" + "Expand button": "Expand", + "No assigned fields configured": "No assigned fields configured" } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index 773c3cd569e..aa1a8263597 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -1393,6 +1393,7 @@ "Collapse settings": "折叠设置", "Upload file settings": "文件上传设置", "Allow selection of existing file": "允许选择已有文件", + "No assigned fields configured": "未配置字段", "Current device type": "当前设备类型", "Computer": "电脑", "Mobile": "手机", diff --git a/packages/core/flow-engine/src/components/variables/VariableInput.tsx b/packages/core/flow-engine/src/components/variables/VariableInput.tsx index 5059af360b1..69c14af17eb 100644 --- a/packages/core/flow-engine/src/components/variables/VariableInput.tsx +++ b/packages/core/flow-engine/src/components/variables/VariableInput.tsx @@ -254,11 +254,29 @@ const VariableInputComponent: React.FC = ({ if (disabled) { return; } - setCurrentMetaTreeNode(null); const cleared = clearValue !== undefined ? clearValue : null; setInnerValue(cleared); + + // 若 clearValue 能解析到某个路径(例如 ['constant']), + // 则尝试立即定位到对应的 MetaTreeNode,以便渲染正确的常量组件。 + try { + const path = resolvePathFromValue?.(cleared); + if (Array.isArray(resolvedMetaTree) && path && path.length > 0) { + const node = findMetaTreeNodeByPath(resolvedMetaTree as MetaTreeNode[], path as string[]); + if (node) { + setCurrentMetaTreeNode(node); + emitChange(cleared as any, node); + return; + } + } + } catch (_) { + // 忽略解析异常,走默认回退 + } + + // 默认回退(无法定位具体 MetaTreeNode 时) + setCurrentMetaTreeNode(null); emitChange(cleared as any); - }, [emitChange, disabled, clearValue]); + }, [emitChange, disabled, clearValue, resolvedMetaTree, resolvePathFromValue]); const stableProps = useMemo(() => { const { style, onFocus, onBlur, disabled, ...otherProps } = restProps; diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateActionModel.tsx b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateActionModel.tsx index 8625d1dbabd..9185309a342 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateActionModel.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/BulkUpdateActionModel.tsx @@ -57,22 +57,9 @@ function AssignFieldsEditor() { formModel.setInitialAssignedValues(prev?.assignedValues || {}); // 批量配置态:移除 ctx.record(Action 为区块级,不具备单条记录上下文) // formModel.context.defineProperty('formValues', { get: () => undefined }); - // formModel.context.defineProperty('record', { - // get: () => action.context.record, - // meta: createCollectionContextMeta( - // () => action.context.dataSourceManager.getDataSource(dsKey).getCollection(collName), - // action.context.t('Current record'), - // ), - // }); - const grid = formModel?.subModels?.grid; - const items = grid?.subModels?.items || []; - for (const it of items) { - const saved = - typeof it?.getStepParams === 'function' ? it.getStepParams('fieldSettings', 'assignValue')?.value : undefined; - if (typeof saved !== 'undefined') { - (it as any).assignValue = saved; - } - } + formModel.context.defineProperty('record', { + get: () => undefined, + }); initializedRef.current = true; }, [action, blockModel?.collection, formModel]); @@ -128,7 +115,7 @@ BulkUpdateActionModel.registerFlow({ defaultParams: { enable: false, title: escapeT('Bulk update'), - content: 'Are you sure you want to proceed with this action?', + content: escapeT('Are you sure you want to perform the Update record action?'), }, }, updateMode: { @@ -158,23 +145,11 @@ BulkUpdateActionModel.registerFlow({ }, async beforeParamsSave(ctx) { const m = ctx.model as BulkUpdateActionModel; - let form: AssignFormModel = (m?.assignFormUid && (ctx.engine.getModel?.(m.assignFormUid) as any)) as any; - if (!form && ctx.engine) { - form = (await ctx.engine.loadModel({ - uid: m.assignFormUid || undefined, - parentId: ctx.model.uid, - subKey: 'assignForm', - })) as any; - } + // 跨视图栈按 uid 定位到设置面板中的真实 AssignForm 实例 + const form: AssignFormModel = (m?.assignFormUid && + (ctx.engine.getModel?.(m.assignFormUid, true) as any)) as any; if (!form) return; const assignedValues = form?.getAssignedValues?.() || {}; - const grid = (form as any)?.subModels?.grid; - const items = grid?.subModels?.items || []; - for (const it of items) { - if (typeof it?.setStepParams === 'function') { - it.setStepParams('fieldSettings', 'assignValue', { value: (it as any).assignValue }); - } - } ctx.model.setStepParams(SETTINGS_FLOW_KEY, 'assignFieldValues', { assignedValues }); }, },