mirror of
https://github.com/nocobase/nocobase.git
synced 2026-09-21 05:44:51 +08:00
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:
@@ -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) {
|
||||
|
||||
+27
-34
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+7
-32
@@ -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 });
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user