@nocobase/plugin-ui-templates 2.0.0-alpha.58 → 2.0.0-alpha.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,8 +8,6 @@
8
8
  */
9
9
  import React from 'react';
10
10
  import { type CreateModelOptions, FlowModel } from '@nocobase/flow-engine';
11
- import { REF_HOST_CTX_KEY } from '../constants';
12
- export { REF_HOST_CTX_KEY };
13
11
  export type ReferenceFormGridTargetSettings = {
14
12
  /** 模板 uid(flowModelTemplates.uid) */
15
13
  templateUid: string;
@@ -22,15 +20,31 @@ export type ReferenceFormGridTargetSettings = {
22
20
  };
23
21
  export declare class ReferenceFormGridModel extends FlowModel {
24
22
  private _scopedEngine?;
23
+ private _targetRoot?;
25
24
  private _targetGrid?;
26
25
  private _resolvedTargetUid?;
27
26
  private _invalidTargetUid?;
27
+ /**
28
+ * 字段模板场景下,模板内 FormItemModel/CollectionFieldModel 的 onInit 会调用 ctx.blockModel.addAppends(fieldPath)。
29
+ * 但模板 root 自身也是一个 CollectionBlockModel,会在未桥接宿主上下文时被识别为 blockModel,导致 appends 写到模板 root 的 resource,
30
+ * 从而宿主表单(如 ApplyFormModel)在刷新记录时缺少关联 appends(例如 users.roles)。
31
+ *
32
+ * 这里在目标 grid 解析完成后扫描字段路径,并把需要的 appends 同步到宿主 block(master + forks),确保关系数据可展示。
33
+ */
34
+ private _syncHostResourceAppends;
28
35
  constructor(options: any);
29
36
  private _ensureScopedEngine;
30
37
  private _getTargetSettings;
31
38
  private _syncHostExtraTitle;
32
39
  addSubModel<T extends FlowModel>(subKey: string, options: CreateModelOptions | T): T;
33
40
  setSubModel(subKey: string, options: CreateModelOptions | FlowModel): FlowModel<import("@nocobase/flow-engine").DefaultStructure>;
41
+ getStepParams(flowKey: string, stepKey: string): any | undefined;
42
+ getStepParams(flowKey: string): Record<string, any> | undefined;
43
+ getStepParams(): Record<string, any>;
44
+ setStepParams(flowKey: string, stepKey: string, params: any): void;
45
+ setStepParams(flowKey: string, stepParams: Record<string, any>): void;
46
+ setStepParams(allParams: Record<string, any>): void;
47
+ saveStepParams(): Promise<any>;
34
48
  onDispatchEventStart(eventName: string): Promise<void>;
35
49
  clearForks(): void;
36
50
  destroy(): Promise<boolean>;
@@ -6,27 +6,14 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
+ import { CommonItemModel } from '@nocobase/client';
9
10
  import { FlowModel, FlowContext } from '@nocobase/flow-engine';
10
11
  type ImporterProps = {
11
- /** 默认从模板根取片段的路径 */
12
- defaultSourcePath?: string;
13
- /** 模板根 use 过滤(可选),支持多个候选 */
14
12
  expectedRootUse?: string | string[];
15
- /** 期望的数据源 key(可选,用于禁用不匹配的模板) */
16
13
  expectedDataSourceKey?: string;
17
- /** 期望的 collectionName(可选,用于禁用不匹配的模板) */
18
14
  expectedCollectionName?: string;
19
- /** 默认挂载到当前模型的 subModels 键(可选),否则使用 importer.subKey */
20
- defaultMountSubKey?: string;
21
- /**
22
- * 引入片段时挂载到第几层父级:
23
- * - 0:挂载到 importer.parent(默认)
24
- * - 1:挂载到 importer.parent.parent
25
- * - 2:以此类推
26
- */
27
- mountToParentLevel?: number;
28
15
  };
29
- export declare class SubModelTemplateImporterModel extends FlowModel {
16
+ export declare class SubModelTemplateImporterModel extends CommonItemModel {
30
17
  props: ImporterProps;
31
18
  resolveExpectedResourceInfo(ctx?: FlowContext, start?: FlowModel): {
32
19
  dataSourceKey?: string;
@@ -7,8 +7,9 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import React from 'react';
10
- import { type FlowEngine, type FlowModel } from '@nocobase/flow-engine';
10
+ import { FlowContext, type FlowEngine, type FlowModel } from '@nocobase/flow-engine';
11
11
  export declare function ensureBlockScopedEngine(flowEngine: FlowEngine, scopedEngine?: FlowEngine): FlowEngine;
12
+ export declare function ensureScopedEngineView(engine: FlowEngine, hostContext?: FlowContext): void;
12
13
  export declare function unlinkScopedEngine(engine?: FlowEngine): void;
13
14
  export declare function renderReferenceTargetPlaceholder(model: {
14
15
  translate?: (key: string, options?: any) => string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { FlowModel } from '@nocobase/flow-engine';
10
+ /**
11
+ * 字段模板 copy 模式只会 duplicate `subModels.grid`,而历史模板可能把部分配置(如布局/连动规则)
12
+ * 存在模板 root 上。reference 模式通过 getStepParams 的 fallback 能读到这些值,但 copy 模式需要
13
+ * 主动把这些 stepParams 合并进 grid 的 stepParams,避免丢失。
14
+ *
15
+ * - 仅在 grid 上缺失对应 stepKey 时才回填(不覆盖 grid 上已有值)
16
+ * - 返回 patched 标记,方便调用方决定是否需要 `saveStepParams()`
17
+ */
18
+ export declare function patchGridOptionsFromTemplateRoot(templateRoot: FlowModel | undefined, gridOptions: any): {
19
+ options: any;
20
+ patched: boolean;
21
+ };
@@ -8,17 +8,17 @@
8
8
  */
9
9
 
10
10
  module.exports = {
11
- "@nocobase/client": "2.0.0-alpha.58",
12
- "@nocobase/flow-engine": "2.0.0-alpha.58",
11
+ "@nocobase/client": "2.0.0-alpha.60",
12
+ "@nocobase/flow-engine": "2.0.0-alpha.60",
13
13
  "react": "18.2.0",
14
14
  "antd": "5.24.2",
15
15
  "@ant-design/icons": "5.6.1",
16
16
  "lodash": "4.17.21",
17
17
  "@formily/react": "2.3.7",
18
- "@nocobase/server": "2.0.0-alpha.58",
19
- "@nocobase/plugin-flow-engine": "2.0.0-alpha.58",
20
- "@nocobase/utils": "2.0.0-alpha.58",
18
+ "@nocobase/server": "2.0.0-alpha.60",
19
+ "@nocobase/plugin-flow-engine": "2.0.0-alpha.60",
20
+ "@nocobase/utils": "2.0.0-alpha.60",
21
21
  "@formily/core": "2.3.7",
22
- "@nocobase/database": "2.0.0-alpha.58",
23
- "@nocobase/actions": "2.0.0-alpha.58"
22
+ "@nocobase/database": "2.0.0-alpha.60",
23
+ "@nocobase/actions": "2.0.0-alpha.60"
24
24
  };
@@ -1,5 +1,5 @@
1
1
  {
2
- "UI templates": "界面模板",
2
+ "UI templates": "UI 模板",
3
3
  "Block templates (v2)": "区块模板 (v2)",
4
4
  "Popup templates (v2)": "弹窗模板 (v2)",
5
5
  "Actions": "操作",
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@nocobase/plugin-ui-templates",
3
3
  "displayName": "UI templates",
4
- "displayName.zh-CN": "界面模板",
4
+ "displayName.zh-CN": "UI 模板",
5
5
  "description": "Provides block templates and popup templates for UI reuse.",
6
6
  "description.zh-CN": "提供区块模板和弹窗模板复用的能力。",
7
- "version": "2.0.0-alpha.58",
7
+ "version": "2.0.0-alpha.60",
8
8
  "license": "AGPL-3.0",
9
9
  "main": "./dist/server/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -33,5 +33,5 @@
33
33
  "block",
34
34
  "popup"
35
35
  ],
36
- "gitHead": "be3acea2fb2a6538a8dbe5b6cc7fa0fd5e15e43b"
36
+ "gitHead": "9113d61ce85b60b7ba3d0e5ca64182d92a15ece4"
37
37
  }
@@ -15,7 +15,6 @@ import { BlockTemplatesPage, PopupTemplatesPage } from './components/FlowModelTe
15
15
  // @ts-ignore
16
16
  import pkg from '../../package.json';
17
17
  import { registerMenuExtensions } from './menuExtensions';
18
- import { registerSubModelMenuExtensions } from './subModelMenuExtensions';
19
18
  import { registerOpenViewPopupTemplateAction } from './openViewActionExtensions';
20
19
 
21
20
  const NAMESPACE = 'ui-templates';
@@ -27,7 +26,6 @@ export class PluginBlockReferenceClient extends Plugin {
27
26
  ReferenceFormGridModel,
28
27
  SubModelTemplateImporterModel,
29
28
  });
30
- registerSubModelMenuExtensions(this.flowEngine);
31
29
  registerOpenViewPopupTemplateAction(this.flowEngine);
32
30
 
33
31
  // 父级菜单(只有标题,无组件)
@@ -22,6 +22,7 @@ import {
22
22
  resolveActionScene,
23
23
  type PopupTemplateContextFlags,
24
24
  } from './utils/templateCompatibility';
25
+ import { patchGridOptionsFromTemplateRoot } from './utils/templateCopy';
25
26
 
26
27
  type MenuItem = {
27
28
  key: string;
@@ -389,18 +390,22 @@ async function handleConvertFieldsToCopy(model: FlowModel, _t: (k: string, opt?:
389
390
  }
390
391
 
391
392
  const duplicated = await model.flowEngine.duplicateModel(gridModel.uid);
393
+ const merged = patchGridOptionsFromTemplateRoot(root, duplicated);
392
394
 
393
395
  // 将复制出的 grid(默认脱离父级)移动到当前表单 grid 位置,避免再走 save 重建整棵树
394
396
  await model.flowEngine.modelRepository.move(duplicated.uid, currentGrid.uid, 'after');
395
397
 
396
398
  const newGrid = model.flowEngine.createModel<FlowModel>({
397
- ...(duplicated as any),
399
+ ...(merged.options as any),
398
400
  parentId: model.uid,
399
401
  subKey: 'grid',
400
402
  subType: 'object',
401
403
  });
402
404
  model.setSubModel('grid', newGrid);
403
405
  await newGrid.afterAddAsSubModel();
406
+ if (merged.patched) {
407
+ await newGrid.saveStepParams();
408
+ }
404
409
 
405
410
  // 引用已清理,回退临时标题(移除“字段模板”标记)
406
411
  const clearTemplateTitle = (m: FlowModel) => {
@@ -531,6 +536,49 @@ async function handleSavePopupAsTemplate(model: FlowModel, _t: (k: string, opt?:
531
536
  const s = String(val).trim();
532
537
  return s ? s : undefined;
533
538
  };
539
+ const resolveDefaultOpenViewResource = (): {
540
+ dataSourceKey?: string;
541
+ collectionName?: string;
542
+ associationName?: string;
543
+ } => {
544
+ const ctx = model.context;
545
+ const field = ctx.collectionField;
546
+ const associationPathName = model.parent?.['associationPathName'];
547
+ const blockModel = ctx.blockModel;
548
+ const fieldCollection = ctx.collection || blockModel?.collection;
549
+ const isAssociationField = (f): boolean => !!f?.isAssociationField?.();
550
+ const associationField =
551
+ !isAssociationField(field) && associationPathName && typeof fieldCollection?.getFieldByPath === 'function'
552
+ ? fieldCollection.getFieldByPath(associationPathName)
553
+ : undefined;
554
+ const assocField = isAssociationField(field) ? field : associationField;
555
+
556
+ if (isAssociationField(assocField)) {
557
+ const targetCollection = assocField?.targetCollection;
558
+ return {
559
+ dataSourceKey: toNonEmptyString(targetCollection?.dataSourceKey),
560
+ collectionName: toNonEmptyString(targetCollection?.name),
561
+ associationName: toNonEmptyString(assocField?.resourceName),
562
+ };
563
+ }
564
+
565
+ const collection = ctx.collection;
566
+ const association = ctx.association;
567
+ return {
568
+ dataSourceKey: toNonEmptyString(collection?.dataSourceKey),
569
+ collectionName: toNonEmptyString(collection?.name),
570
+ associationName: toNonEmptyString(field?.target || association?.resourceName),
571
+ };
572
+ };
573
+
574
+ // 兼容历史数据:openViewParams 里固化的 associationName 可能是错的。
575
+ // 当能从上下文推导出 associationName 时(关联字段/关联字段属性),优先使用推导值。
576
+ const defaultOpenViewResource = resolveDefaultOpenViewResource();
577
+ const templateDataSourceKey =
578
+ defaultOpenViewResource.dataSourceKey || toNonEmptyString(openViewParams?.dataSourceKey);
579
+ const templateCollectionName =
580
+ defaultOpenViewResource.collectionName || toNonEmptyString(openViewParams?.collectionName);
581
+ const templateAssociationName = defaultOpenViewResource.associationName;
534
582
  const getDefaultFilterByTkExpr = (): string | undefined => {
535
583
  // 与 openView 默认行为对齐:尽量落表达式而非具体值
536
584
  const recordKeyPath = model.context?.collection?.filterTargetKey || 'id';
@@ -538,7 +586,7 @@ async function handleSavePopupAsTemplate(model: FlowModel, _t: (k: string, opt?:
538
586
  };
539
587
  const getDefaultSourceIdExpr = (): string | undefined => {
540
588
  // 如果有 associationName,说明是关系资源弹窗,默认需要 sourceId
541
- if (toNonEmptyString(openViewParams?.associationName)) {
589
+ if (templateAssociationName) {
542
590
  return `{{ ctx.resource.sourceId }}`;
543
591
  }
544
592
  try {
@@ -585,9 +633,9 @@ async function handleSavePopupAsTemplate(model: FlowModel, _t: (k: string, opt?:
585
633
  targetUid: values.targetUid,
586
634
  useModel: model.use,
587
635
  type: 'popup',
588
- dataSourceKey: openViewParams?.dataSourceKey,
589
- collectionName: openViewParams?.collectionName,
590
- associationName: openViewParams?.associationName,
636
+ dataSourceKey: templateDataSourceKey,
637
+ collectionName: templateCollectionName,
638
+ associationName: templateAssociationName,
591
639
  filterByTk: templateFilterByTk,
592
640
  sourceId: templateSourceId,
593
641
  };
@@ -25,6 +25,7 @@ import {
25
25
  ensureBlockScopedEngine,
26
26
  ReferenceScopedRenderer,
27
27
  renderReferenceTargetPlaceholder,
28
+ ensureScopedEngineView,
28
29
  unlinkScopedEngine,
29
30
  } from './referenceShared';
30
31
 
@@ -133,6 +134,9 @@ export class ReferenceBlockModel extends BlockModel {
133
134
 
134
135
  private _ensureScopedEngine(): FlowEngine {
135
136
  this._scopedEngine = ensureBlockScopedEngine(this.flowEngine, this._scopedEngine);
137
+ // 引用区块会在 scoped engine 中 loadModel,目标模型的 onInit 可能会读取 ctx.view。
138
+ // 部分场景(如审批配置)view 仅存在于宿主模型上下文而非 engine.context,需要显式桥接。
139
+ ensureScopedEngineView(this._scopedEngine, this.context as any);
136
140
  return this._scopedEngine;
137
141
  }
138
142
 
@@ -9,21 +9,22 @@
9
9
 
10
10
  import React from 'react';
11
11
  import _ from 'lodash';
12
- import { type CreateModelOptions, FlowEngine, FlowModel } from '@nocobase/flow-engine';
13
- import { FormGridModel } from '@nocobase/client';
12
+ import { type CreateModelOptions, FlowContext, FlowEngine, FlowModel } from '@nocobase/flow-engine';
14
13
  import { REF_HOST_CTX_KEY } from '../constants';
15
14
  import { NAMESPACE } from '../locale';
16
15
  import {
17
16
  ensureBlockScopedEngine,
18
17
  ReferenceScopedRenderer,
19
18
  renderReferenceTargetPlaceholder,
19
+ ensureScopedEngineView,
20
20
  unlinkScopedEngine,
21
21
  } from './referenceShared';
22
22
 
23
23
  const SETTINGS_FLOW_KEY = 'referenceSettings';
24
24
  const SETTINGS_STEP_KEY = 'useTemplate';
25
25
 
26
- export { REF_HOST_CTX_KEY };
26
+ /** 标记已添加 host context bridge,避免重复添加 */
27
+ const BRIDGE_MARKER = Symbol.for('nocobase.refGridHostBridge');
27
28
 
28
29
  export type ReferenceFormGridTargetSettings = {
29
30
  /** 模板 uid(flowModelTemplates.uid) */
@@ -36,25 +37,58 @@ export type ReferenceFormGridTargetSettings = {
36
37
  targetPath?: string;
37
38
  };
38
39
 
39
- type ReferenceHostInfo = {
40
- hostUid?: string;
41
- hostUse?: string;
42
- ref: {
43
- templateUid: string;
44
- templateName?: string;
45
- targetUid: string;
46
- targetPath: string;
47
- mountSubKey: 'grid';
48
- mode: 'reference';
49
- };
50
- };
51
-
52
40
  export class ReferenceFormGridModel extends FlowModel {
53
41
  private _scopedEngine?: FlowEngine;
42
+ private _targetRoot?: FlowModel;
54
43
  private _targetGrid?: FlowModel;
55
44
  private _resolvedTargetUid?: string;
56
45
  private _invalidTargetUid?: string;
57
46
 
47
+ /**
48
+ * 字段模板场景下,模板内 FormItemModel/CollectionFieldModel 的 onInit 会调用 ctx.blockModel.addAppends(fieldPath)。
49
+ * 但模板 root 自身也是一个 CollectionBlockModel,会在未桥接宿主上下文时被识别为 blockModel,导致 appends 写到模板 root 的 resource,
50
+ * 从而宿主表单(如 ApplyFormModel)在刷新记录时缺少关联 appends(例如 users.roles)。
51
+ *
52
+ * 这里在目标 grid 解析完成后扫描字段路径,并把需要的 appends 同步到宿主 block(master + forks),确保关系数据可展示。
53
+ */
54
+ private _syncHostResourceAppends(host: FlowModel, targetGrid: FlowModel) {
55
+ if (!host['addAppends']) {
56
+ return;
57
+ }
58
+ const candidates = new Set<string>();
59
+ const addCandidate = (val: unknown) => {
60
+ const s = typeof val === 'string' ? val.trim() : '';
61
+ if (!s) return;
62
+ candidates.add(s);
63
+ const top = s.split('.')[0]?.trim();
64
+ if (top) candidates.add(top);
65
+ };
66
+
67
+ const visit = (m: FlowModel) => {
68
+ const init = m.getStepParams?.('fieldSettings', 'init') as
69
+ | { fieldPath?: string; associationPathName?: string }
70
+ | undefined;
71
+ if (init) {
72
+ addCandidate(init.fieldPath);
73
+ addCandidate(init.associationPathName);
74
+ }
75
+ const subs = m.subModels || {};
76
+ for (const v of Object.values(subs)) {
77
+ if (Array.isArray(v)) {
78
+ v.forEach((c) => c instanceof FlowModel && visit(c));
79
+ } else if (v instanceof FlowModel) {
80
+ visit(v);
81
+ }
82
+ }
83
+ };
84
+
85
+ visit(targetGrid);
86
+ if (candidates.size === 0) return;
87
+ for (const fieldPath of candidates) {
88
+ (host as any).addAppends?.(fieldPath);
89
+ }
90
+ }
91
+
58
92
  constructor(options: any) {
59
93
  super(options);
60
94
 
@@ -167,12 +201,100 @@ export class ReferenceFormGridModel extends FlowModel {
167
201
  return this._targetGrid.setSubModel(subKey, options);
168
202
  }
169
203
 
204
+ getStepParams(flowKey: string, stepKey: string): any | undefined;
205
+ getStepParams(flowKey: string): Record<string, any> | undefined;
206
+ getStepParams(): Record<string, any>;
207
+ getStepParams(flowKey?: string, stepKey?: string): any {
208
+ if (!flowKey || flowKey === SETTINGS_FLOW_KEY) {
209
+ return super.getStepParams(flowKey, stepKey);
210
+ }
211
+
212
+ if (!this._targetGrid) {
213
+ // 未解析完成:允许读取本地 stepParams,避免配置对话框/中间态丢值
214
+ return super.getStepParams(flowKey, stepKey);
215
+ }
216
+
217
+ if (stepKey) {
218
+ const fromGrid = this._targetGrid.getStepParams(flowKey, stepKey);
219
+ if (typeof fromGrid !== 'undefined') return fromGrid;
220
+ return this._targetRoot?.getStepParams?.(flowKey, stepKey);
221
+ }
222
+
223
+ const gridFlow = this._targetGrid.getStepParams(flowKey) as any;
224
+ const rootFlow = this._targetRoot?.getStepParams?.(flowKey) as any;
225
+ if (rootFlow && typeof rootFlow === 'object') {
226
+ return { ...rootFlow, ...(gridFlow || {}) };
227
+ }
228
+ return gridFlow;
229
+ }
230
+
231
+ setStepParams(flowKey: string, stepKey: string, params: any): void;
232
+ setStepParams(flowKey: string, stepParams: Record<string, any>): void;
233
+ setStepParams(allParams: Record<string, any>): void;
234
+ setStepParams(flowKeyOrAllParams: any, stepKeyOrStepsParams?: any, params?: any): void {
235
+ if (typeof flowKeyOrAllParams === 'string') {
236
+ const flowKey = flowKeyOrAllParams;
237
+ if (flowKey === SETTINGS_FLOW_KEY || !this._targetGrid) {
238
+ super.setStepParams(flowKeyOrAllParams, stepKeyOrStepsParams, params);
239
+ return;
240
+ }
241
+ if (typeof stepKeyOrStepsParams === 'string' && params !== undefined) {
242
+ this._targetGrid.setStepParams(flowKey, stepKeyOrStepsParams, params);
243
+ return;
244
+ }
245
+ if (typeof stepKeyOrStepsParams === 'object' && stepKeyOrStepsParams !== null) {
246
+ this._targetGrid.setStepParams(flowKey, stepKeyOrStepsParams);
247
+ }
248
+ return;
249
+ }
250
+
251
+ if (typeof flowKeyOrAllParams === 'object' && flowKeyOrAllParams !== null) {
252
+ const allParams = flowKeyOrAllParams as Record<string, any>;
253
+ const localAll: Record<string, any> = {};
254
+ const delegatedAll: Record<string, any> = {};
255
+ for (const [fk, steps] of Object.entries(allParams)) {
256
+ if (fk === SETTINGS_FLOW_KEY || !this._targetGrid) {
257
+ localAll[fk] = steps;
258
+ } else {
259
+ delegatedAll[fk] = steps;
260
+ }
261
+ }
262
+ if (Object.keys(localAll).length > 0) {
263
+ super.setStepParams(localAll);
264
+ }
265
+ if (Object.keys(delegatedAll).length > 0 && this._targetGrid) {
266
+ this._targetGrid.setStepParams(delegatedAll);
267
+ }
268
+ }
269
+ }
270
+
271
+ async saveStepParams() {
272
+ // 如果目标尚未解析,先触发解析
273
+ if (!this._targetGrid) {
274
+ await this.dispatchEvent('beforeRender');
275
+ }
276
+ // 将本地非 settings 参数刷新到目标 grid
277
+ if (this._targetGrid && this.stepParams) {
278
+ for (const [flowKey, steps] of Object.entries(this.stepParams)) {
279
+ if (flowKey === SETTINGS_FLOW_KEY || typeof steps !== 'object' || steps === null) continue;
280
+ this._targetGrid.setStepParams(flowKey, steps as Record<string, any>);
281
+ delete (this.stepParams as Record<string, any>)[flowKey];
282
+ }
283
+ }
284
+ const res = await super.saveStepParams();
285
+ if (this._targetGrid) {
286
+ await this._targetGrid.saveStepParams();
287
+ }
288
+ return res;
289
+ }
290
+
170
291
  public async onDispatchEventStart(eventName: string): Promise<void> {
171
292
  if (eventName !== 'beforeRender') return;
172
293
 
173
294
  const settings = this._getTargetSettings();
174
295
  if (!settings) {
175
296
  this._syncHostExtraTitle(undefined);
297
+ this._targetRoot = undefined;
176
298
  this._targetGrid = undefined;
177
299
  this._resolvedTargetUid = undefined;
178
300
  this._invalidTargetUid = undefined;
@@ -195,6 +317,8 @@ export class ReferenceFormGridModel extends FlowModel {
195
317
  }
196
318
 
197
319
  const engine = this._ensureScopedEngine();
320
+ const host = this.parent as FlowModel | undefined;
321
+ ensureScopedEngineView(engine, (host?.context as any) || (this.context as any));
198
322
  const targetUid = settings.targetUid;
199
323
  const prevTargetGrid = this._targetGrid;
200
324
  const prevResolvedTargetUid = this._resolvedTargetUid;
@@ -205,12 +329,12 @@ export class ReferenceFormGridModel extends FlowModel {
205
329
  // 在“模板引用”切换的中间态(例如模型树刚替换、上下文尚未稳定)下,
206
330
  // 可能出现首次解析不到目标(短暂返回 null/undefined)。这里做一次轻量重试,
207
331
  // 避免界面闪现 “Target block is invalid” 占位。
208
- const tryResolveTargetGrid = async (): Promise<FlowModel | undefined> => {
332
+ const tryResolveTargetGrid = async (): Promise<{ root: FlowModel; grid: FlowModel } | undefined> => {
209
333
  const root = await engine.loadModel<FlowModel>({ uid: targetUid });
210
334
  if (!root) return undefined;
211
335
 
212
- const host = this.parent as FlowModel | undefined;
213
- const hostInfo: ReferenceHostInfo = {
336
+ root.setParent(host);
337
+ const hostInfo = {
214
338
  hostUid: host?.uid,
215
339
  hostUse: host?.use,
216
340
  ref: {
@@ -224,23 +348,43 @@ export class ReferenceFormGridModel extends FlowModel {
224
348
  };
225
349
  root.context.defineProperty(REF_HOST_CTX_KEY, { value: hostInfo });
226
350
 
227
- const fragment = _.get(root as any, targetPath);
228
- const gridModel =
229
- fragment instanceof FlowModel ? fragment : _.castArray(fragment).find((m) => m instanceof FlowModel);
230
- return gridModel || undefined;
351
+ const fragment = root.subModels?.grid;
352
+ let gridModel: FlowModel | undefined;
353
+ if (fragment instanceof FlowModel) {
354
+ gridModel = fragment;
355
+ }
356
+ // 将宿主区块上下文注入到被引用的 grid:
357
+ // - Details 区块字段渲染依赖 ctx.record/resource/blockModel 等(定义在宿主 block context 上);
358
+ // - 但同时要保留 scoped engine(ctx.engine)指向,避免丢失实例/缓存隔离。
359
+ // 注意:使用 Symbol 标记避免重复添加 delegate(beforeRender 可能多次触发)
360
+ const contextWithMarker = gridModel?.context as (FlowContext & { [BRIDGE_MARKER]?: boolean }) | undefined;
361
+ if (gridModel && host?.context && !contextWithMarker?.[BRIDGE_MARKER]) {
362
+ const bridge = new FlowContext();
363
+ bridge.defineProperty('engine', { value: engine });
364
+ bridge.addDelegate(host.context);
365
+ gridModel.context.addDelegate(bridge);
366
+ (gridModel.context as FlowContext & { [BRIDGE_MARKER]?: boolean })[BRIDGE_MARKER] = true;
367
+ }
368
+ if (!gridModel) return undefined;
369
+ // 同步模板字段需要的关联 appends 到宿主 block 的 resource(避免 users.roles 等关系字段为空)
370
+ if (host) {
371
+ this._syncHostResourceAppends(host, gridModel);
372
+ }
373
+ return { root, grid: gridModel };
231
374
  };
232
375
 
233
- let gridModel = await tryResolveTargetGrid();
234
- if (!gridModel) {
376
+ let resolved = await tryResolveTargetGrid();
377
+ if (!resolved) {
235
378
  await new Promise((resolve) => setTimeout(resolve, 50));
236
379
  const latest = this._getTargetSettings();
237
380
  if (latest?.targetUid !== targetUid) {
238
381
  return;
239
382
  }
240
- gridModel = await tryResolveTargetGrid();
383
+ resolved = await tryResolveTargetGrid();
241
384
  }
242
385
 
243
- if (!gridModel) {
386
+ if (!resolved) {
387
+ this._targetRoot = undefined;
244
388
  this._targetGrid = undefined;
245
389
  this._resolvedTargetUid = undefined;
246
390
  this._invalidTargetUid = targetUid;
@@ -250,10 +394,12 @@ export class ReferenceFormGridModel extends FlowModel {
250
394
  return;
251
395
  }
252
396
 
253
- this._targetGrid = gridModel;
397
+ this._targetRoot = resolved.root;
398
+ this._targetGrid = resolved.grid;
254
399
  this._resolvedTargetUid = targetUid;
255
400
  this._invalidTargetUid = undefined;
256
- if (prevTargetGrid !== gridModel || prevResolvedTargetUid !== targetUid || prevInvalidTargetUid) {
401
+
402
+ if (prevTargetGrid !== resolved.grid || prevResolvedTargetUid !== targetUid || prevInvalidTargetUid) {
257
403
  this.rerender();
258
404
  }
259
405
  }