fix: assign values and update record action not working (#7565)

* fix: assign values operation setting render error

* fix: update action and assign values action

* test: add tests

* fix: bug and i18n

* fix: formula field
This commit is contained in:
gchust
2025-10-12 14:46:45 +08:00
committed by GitHub
parent 22f36bca84
commit b318de2b87
14 changed files with 183 additions and 77 deletions
@@ -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) {
@@ -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 ? <FlowModelRenderer model={formModel} showFlowSettings={false} /> : null;
}
export class UpdateActionModel extends ActionModel<{
function Info() {
const ctx = useFlowSettingsContext();
return (
<Alert
type="info"
showIcon
message={ctx.t(
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
)}
/>
);
}
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': () => (
<Alert type="info" showIcon message={'点击当前自定义按钮时,当前数据以下字段将按照以下表单保存。'} />
),
'x-component': () => <Info />,
},
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: {
@@ -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<string, any> = {};
setAssignedValues(v: Record<string, any>) {
this._values = v || {};
}
getAssignedValues(): Record<string, any> {
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<UpdateRecordActionModel>({ use: 'UpdateRecordActionModel', uid: 'act-u' });
const form = root.createModel<TestAssignFormModel>({
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 });
});
});
@@ -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';
//
@@ -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') {
@@ -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={''}
/>
</FormItem>
);
@@ -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
@@ -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 - 专门用于新增记录
@@ -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';
@@ -47,6 +47,16 @@ export class VariableFieldFormModel extends FlowModel {
<FormProvider form={this.form}>
<FormLayout layout={'vertical'}>
{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 <FlowModelRenderer key={field.uid} model={field} />;
})}
</FormLayout>
+2 -1
View File
@@ -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"
}
@@ -1393,6 +1393,7 @@
"Collapse settings": "折叠设置",
"Upload file settings": "文件上传设置",
"Allow selection of existing file": "允许选择已有文件",
"No assigned fields configured": "未配置字段",
"Current device type": "当前设备类型",
"Computer": "电脑",
"Mobile": "手机",
@@ -254,11 +254,29 @@ const VariableInputComponent: React.FC<VariableInputProps> = ({
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;
@@ -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 });
},
},