@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.7

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.
Files changed (121) hide show
  1. package/es/PluginSettingsManager.d.ts +33 -0
  2. package/es/RouteRepository.d.ts +21 -6
  3. package/es/components/form/JsonTextArea.d.ts +2 -1
  4. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  5. package/es/components/form/index.d.ts +1 -0
  6. package/es/components/index.d.ts +1 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  11. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  12. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  13. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  14. package/es/flow/models/base/GridModel.d.ts +1 -1
  15. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  16. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  17. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  19. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  20. package/es/flow-compat/FieldValidation.d.ts +2 -1
  21. package/es/index.d.ts +2 -1
  22. package/es/index.mjs +145 -119
  23. package/lib/index.js +148 -122
  24. package/package.json +7 -7
  25. package/src/PluginSettingsManager.ts +53 -0
  26. package/src/RouteRepository.ts +126 -24
  27. package/src/__tests__/PluginSettingsManager.test.ts +13 -0
  28. package/src/__tests__/RouteRepository.test.ts +216 -0
  29. package/src/__tests__/exports.test.ts +16 -0
  30. package/src/__tests__/plugin-manager.test.tsx +44 -2
  31. package/src/__tests__/settings-center.test.tsx +40 -1
  32. package/src/components/form/JsonTextArea.tsx +21 -11
  33. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  34. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  35. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  36. package/src/components/form/index.tsx +1 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  39. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +58 -0
  40. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  41. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  42. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  43. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  44. package/src/flow/actions/customVariable.tsx +1 -2
  45. package/src/flow/actions/dataScopeFilter.ts +10 -0
  46. package/src/flow/actions/linkageRules.tsx +122 -34
  47. package/src/flow/actions/runjs.tsx +2 -14
  48. package/src/flow/actions/validation.tsx +62 -30
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  51. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  52. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  53. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  55. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  56. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  57. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  58. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  59. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  60. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  61. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  62. package/src/flow/components/DefaultValue.tsx +1 -2
  63. package/src/flow/components/FieldAssignRulesEditor.tsx +128 -21
  64. package/src/flow/components/FlowRoute.tsx +56 -11
  65. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +508 -4
  66. package/src/flow/components/__tests__/fieldAssignOptions.test.ts +151 -3
  67. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  68. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  69. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  70. package/src/flow/components/fieldAssignOptions.ts +155 -27
  71. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  72. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  73. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  74. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  75. package/src/flow/models/base/ActionModel.tsx +12 -1
  76. package/src/flow/models/base/GridModel.tsx +38 -7
  77. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  78. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  79. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  80. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  81. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  82. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  83. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  84. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  85. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  86. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  87. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  88. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  89. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  90. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  91. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  92. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  93. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  94. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  95. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  96. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  97. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  98. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  99. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  100. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  101. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  102. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  103. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  104. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  105. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  106. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  107. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  108. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  109. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  110. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  111. package/src/flow-compat/FieldValidation.tsx +122 -60
  112. package/src/flow-compat/Popover.tsx +43 -4
  113. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  114. package/src/index.ts +8 -1
  115. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  116. package/src/nocobase-buildin-plugin/index.tsx +0 -2
  117. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  118. package/src/settings-center/SystemSettingsPage.tsx +0 -1
  119. package/src/settings-center/plugin-manager/PluginCard.tsx +2 -2
  120. package/src/settings-center/plugin-manager/index.tsx +3 -0
  121. package/src/settings-center/utils.tsx +0 -6
@@ -14,6 +14,15 @@ import {
14
14
  collectFieldAssignCascaderOptions,
15
15
  } from '../fieldAssignOptions';
16
16
 
17
+ type TestCollectionField = {
18
+ name?: string;
19
+ title?: string;
20
+ interface?: string;
21
+ target?: string;
22
+ targetCollection?: unknown;
23
+ isAssociationField?: () => boolean;
24
+ };
25
+
17
26
  describe('fieldAssignOptions', () => {
18
27
  it('marks subform node as non-leaf so Cascader can lazy-load target collection fields', () => {
19
28
  const usersCollection = {
@@ -98,8 +107,58 @@ describe('fieldAssignOptions', () => {
98
107
  ).toBe(true);
99
108
  });
100
109
 
110
+ it('preserves configured display association field paths as nested cascader options', () => {
111
+ const orgNameField = {
112
+ name: 'orgname',
113
+ title: '公司名称(单行文本)',
114
+ interface: 'input',
115
+ type: 'string',
116
+ isAssociationField: () => false,
117
+ };
118
+ const orgCollection = {
119
+ getFields: () => [orgNameField],
120
+ getField: (name: string) => (name === 'orgname' ? orgNameField : null),
121
+ };
122
+ const orgAssociationField = {
123
+ name: 'org_m2o',
124
+ title: 'org_m2o',
125
+ interface: 'm2o',
126
+ type: 'belongsTo',
127
+ target: 'org',
128
+ isAssociationField: () => true,
129
+ targetCollection: orgCollection,
130
+ };
131
+ const rootCollection = {
132
+ getField: (name: string) => (name === 'org_m2o' ? orgAssociationField : null),
133
+ getFields: () => [orgAssociationField],
134
+ };
135
+ const displayAssociationItemModel = {
136
+ props: { label: '公司名称(单行文本)' },
137
+ getStepParams: (flowKey: string, stepKey: string) => {
138
+ if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath: 'org_m2o.orgname' };
139
+ return {};
140
+ },
141
+ subModels: {
142
+ field: {
143
+ context: { collectionField: orgNameField },
144
+ },
145
+ },
146
+ };
147
+ const formBlockModel = {
148
+ context: { collection: rootCollection },
149
+ subModels: { grid: { subModels: { items: [displayAssociationItemModel] } } },
150
+ };
151
+
152
+ const options = collectFieldAssignCascaderOptions({ formBlockModel, t: (s) => s });
153
+ const orgOption = options.find((option) => option.value === 'org_m2o');
154
+
155
+ expect(orgOption?.isLeaf).toBe(false);
156
+ expect(orgOption?.children?.some((child) => child.value === 'orgname' && child.isLeaf === true)).toBe(true);
157
+ expect(options.some((option) => option.value === 'orgname')).toBe(false);
158
+ });
159
+
101
160
  it('hides configured association fields deeper than relation / relation / field', () => {
102
- const makeItem = (fieldPath: string, field: any, label: string) => ({
161
+ const makeItem = (fieldPath: string, field: TestCollectionField, label: string) => ({
103
162
  props: { label },
104
163
  getStepParams: (flowKey: string, stepKey: string) => {
105
164
  if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath };
@@ -141,9 +200,98 @@ describe('fieldAssignOptions', () => {
141
200
  };
142
201
 
143
202
  const options = collectFieldAssignCascaderOptions({ formBlockModel, t: (s) => s });
203
+ const usersOption = options.find((item) => item.value === 'users');
204
+ const departmentOption = usersOption?.children?.find((item) => item.value === 'department');
205
+
206
+ expect(departmentOption?.children?.map((item) => item.value)).toContain('name');
207
+ expect(departmentOption?.children?.map((item) => item.value)).not.toContain('company');
208
+ });
209
+
210
+ it('hides configured non-association fields under a third association level', () => {
211
+ const codeField = {
212
+ name: 'code',
213
+ title: 'Code',
214
+ interface: 'input',
215
+ isAssociationField: () => false,
216
+ };
217
+ const companyCollection = {
218
+ getField: (name: string) => (name === 'code' ? codeField : null),
219
+ getFields: () => [codeField],
220
+ };
221
+ const companyField = {
222
+ name: 'company',
223
+ title: 'Company',
224
+ interface: 'm2o',
225
+ target: 'companies',
226
+ isAssociationField: () => true,
227
+ targetCollection: companyCollection,
228
+ };
229
+ const nameField = {
230
+ name: 'name',
231
+ title: 'Name',
232
+ interface: 'input',
233
+ isAssociationField: () => false,
234
+ };
235
+ const departmentCollection = {
236
+ getField: (name: string) => (name === 'name' ? nameField : name === 'company' ? companyField : null),
237
+ getFields: () => [nameField, companyField],
238
+ };
239
+ const departmentField = {
240
+ name: 'department',
241
+ title: 'Department',
242
+ interface: 'm2o',
243
+ target: 'departments',
244
+ isAssociationField: () => true,
245
+ targetCollection: departmentCollection,
246
+ };
247
+ const userCollection = {
248
+ getField: (name: string) => (name === 'department' ? departmentField : null),
249
+ getFields: () => [departmentField],
250
+ };
251
+ const usersField = {
252
+ name: 'users',
253
+ title: 'Users',
254
+ interface: 'm2m',
255
+ target: 'users',
256
+ isAssociationField: () => true,
257
+ targetCollection: userCollection,
258
+ };
259
+ const rootCollection = {
260
+ getField: (name: string) => (name === 'users' ? usersField : null),
261
+ getFields: () => [usersField],
262
+ };
263
+ const makeItem = (fieldPath: string, field: TestCollectionField, label: string) => ({
264
+ props: { label },
265
+ getStepParams: (flowKey: string, stepKey: string) => {
266
+ if (flowKey === 'fieldSettings' && stepKey === 'init') return { fieldPath };
267
+ return {};
268
+ },
269
+ subModels: {
270
+ field: {
271
+ context: { collectionField: field },
272
+ },
273
+ },
274
+ });
275
+ const formBlockModel = {
276
+ context: { collection: rootCollection },
277
+ subModels: {
278
+ grid: {
279
+ subModels: {
280
+ items: [
281
+ makeItem('users.department.name', nameField, 'Department name'),
282
+ makeItem('users.department.company.code', codeField, 'Company code'),
283
+ ],
284
+ },
285
+ },
286
+ },
287
+ };
288
+
289
+ const options = collectFieldAssignCascaderOptions({ formBlockModel, t: (s) => s });
290
+ const usersOption = options.find((item) => item.value === 'users');
291
+ const departmentOption = usersOption?.children?.find((item) => item.value === 'department');
144
292
 
145
- expect(options.map((item) => item.value)).toContain('name');
146
- expect(options.map((item) => item.value)).not.toContain('company');
293
+ expect(departmentOption?.children?.map((item) => item.value)).toContain('name');
294
+ expect(departmentOption?.children?.map((item) => item.value)).not.toContain('company');
147
295
  });
148
296
 
149
297
  it('hides lazy-loaded association selector options after two association levels', () => {
@@ -20,8 +20,6 @@ import {
20
20
  FlowEngineProvider,
21
21
  FlowModelRenderer,
22
22
  ElementProxy,
23
- createSafeWindow,
24
- createSafeDocument,
25
23
  createViewScopedEngine,
26
24
  } from '@nocobase/flow-engine';
27
25
  import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
@@ -167,11 +165,7 @@ describe('useCodeRunner (beforeRender)', () => {
167
165
  const code = ctx?.inputArgs?.preview?.code || '';
168
166
  ctx.onRefReady(ctx.ref, async (el) => {
169
167
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
170
- await ctx.runjs(
171
- code,
172
- { window: createSafeWindow(), document: createSafeDocument() },
173
- { preprocessTemplates: true },
174
- );
168
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
175
169
  });
176
170
  },
177
171
  },
@@ -286,16 +280,7 @@ return currentUsername;
286
280
  const code = ctx?.inputArgs?.preview?.code || '';
287
281
  ctx.onRefReady(ctx.ref, async (el) => {
288
282
  ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
289
- const navigator = { userAgent: 'test' } as any;
290
- await ctx.runjs(
291
- code,
292
- {
293
- window: createSafeWindow({ navigator }),
294
- document: createSafeDocument(),
295
- navigator,
296
- },
297
- { preprocessTemplates: true },
298
- );
283
+ await ctx.runjs(code, undefined, { preprocessTemplates: true });
299
284
  });
300
285
  },
301
286
  },
@@ -9,14 +9,7 @@
9
9
 
10
10
  import { useCallback, useRef, useState } from 'react';
11
11
  import { parseErrorLineColumn, WRAPPER_PRELUDE_LINES } from '../errorHelpers';
12
- import {
13
- FlowModelContext,
14
- JSRunner,
15
- createSafeWindow,
16
- createSafeDocument,
17
- createSafeNavigator,
18
- prepareRunJsCode,
19
- } from '@nocobase/flow-engine';
12
+ import { FlowModelContext, JSRunner, prepareRunJsCode } from '@nocobase/flow-engine';
20
13
 
21
14
  export type RunLog = { level: 'log' | 'info' | 'warn' | 'error'; msg: string; line?: number; column?: number };
22
15
  type RunResult = Awaited<ReturnType<JSRunner['run']>>;
@@ -286,12 +279,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
286
279
  const eventName = typeof onDef === 'string' ? onDef : onDef?.eventName;
287
280
  if (!flow) {
288
281
  // 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
289
- const navigator = createSafeNavigator();
290
- await directRunCtx.runjs(
291
- code,
292
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
293
- { version },
294
- );
282
+ await directRunCtx.runjs(code, undefined, { version });
295
283
  } else if (typeof eventName === 'string') {
296
284
  await m.dispatchEvent(eventName, { preview: { code } }, { sequential: true, useCache: false });
297
285
  } else if (isManual) {
@@ -20,10 +20,8 @@ import * as acornWalk from 'acorn-walk';
20
20
  import {
21
21
  JSRunner,
22
22
  FlowContext,
23
- createSafeDocument,
24
- createSafeNavigator,
25
- createSafeWindow,
26
23
  prepareRunJsCode,
24
+ RUNJS_ALLOWED_BARE_GLOBAL_NAMES,
27
25
  shouldPreprocessRunJSTemplates,
28
26
  } from '@nocobase/flow-engine';
29
27
 
@@ -604,41 +602,7 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
604
602
  return issues;
605
603
  }
606
604
 
607
- const declared = new Set<string>([
608
- // Common globals / allowed runtime context roots
609
- 'ctx',
610
- 'console',
611
- 'window',
612
- 'document',
613
- 'navigator',
614
- 'Math',
615
- 'Date',
616
- 'Array',
617
- 'Object',
618
- 'Number',
619
- 'String',
620
- 'Boolean',
621
- 'Promise',
622
- 'RegExp',
623
- 'Set',
624
- 'Map',
625
- 'WeakSet',
626
- 'WeakMap',
627
- 'JSON',
628
- 'Intl',
629
- 'URL',
630
- 'Error',
631
- 'TypeError',
632
- 'encodeURIComponent',
633
- 'decodeURIComponent',
634
- 'parseInt',
635
- 'parseFloat',
636
- 'isNaN',
637
- 'isFinite',
638
- 'undefined',
639
- 'NaN',
640
- 'Infinity',
641
- ]);
605
+ const declared = new Set<string>(RUNJS_ALLOWED_BARE_GLOBAL_NAMES);
642
606
 
643
607
  const addId = (id: any) => {
644
608
  if (id && typeof id.name === 'string') declared.add(id.name);
@@ -1036,15 +1000,14 @@ export async function diagnoseRunJS(
1036
1000
  const { consoleCapture, loggerCapture } = createLogCollectors(logs);
1037
1001
 
1038
1002
  const baseGlobals: Record<string, any> = { console: consoleCapture };
1039
- try {
1040
- if (typeof window !== 'undefined') {
1041
- const navigator = createSafeNavigator();
1003
+ if (typeof window !== 'undefined') {
1004
+ baseGlobals.window = window;
1005
+ if (typeof navigator !== 'undefined') {
1042
1006
  baseGlobals.navigator = navigator;
1043
- baseGlobals.window = createSafeWindow({ navigator });
1044
- baseGlobals.document = createSafeDocument();
1045
1007
  }
1046
- } catch (_) {
1047
- // ignore safe globals failures
1008
+ }
1009
+ if (typeof document !== 'undefined') {
1010
+ baseGlobals.document = document;
1048
1011
  }
1049
1012
 
1050
1013
  let prepared = src;
@@ -57,12 +57,11 @@ function getItemLabel(itemModel: any, t: (key: string) => string): string {
57
57
  return '';
58
58
  }
59
59
 
60
- function getLastPathSegment(path: string): string {
61
- const segs = String(path || '')
60
+ function splitFieldPath(path: string): string[] {
61
+ return String(path || '')
62
62
  .split('.')
63
63
  .map((s) => s.trim())
64
64
  .filter(Boolean);
65
- return segs.length ? segs[segs.length - 1] : '';
66
65
  }
67
66
 
68
67
  type CollectionLike = {
@@ -70,6 +69,15 @@ type CollectionLike = {
70
69
  getFields?: () => unknown[];
71
70
  };
72
71
 
72
+ type CollectionFieldLike = {
73
+ name?: unknown;
74
+ title?: unknown;
75
+ interface?: unknown;
76
+ target?: unknown;
77
+ isAssociationField?: () => boolean;
78
+ targetCollection?: CollectionLike | null;
79
+ };
80
+
73
81
  function getCollectionFromFormBlockModel(model: any): CollectionLike | null {
74
82
  if (!model || typeof model !== 'object') return null;
75
83
  const collection = (model as any)?.collection || (model as any)?.context?.collection;
@@ -89,14 +97,7 @@ export function buildFieldAssignCascaderOptionsFromCollection(
89
97
  const out: FieldAssignCascaderOption[] = [];
90
98
  for (const rawField of fields) {
91
99
  if (!rawField) continue;
92
- const f = rawField as {
93
- name?: unknown;
94
- title?: unknown;
95
- interface?: unknown;
96
- target?: unknown;
97
- isAssociationField?: () => boolean;
98
- targetCollection?: any;
99
- };
100
+ const f = rawField as CollectionFieldLike;
100
101
  const fieldInterface = typeof f.interface === 'string' ? f.interface : undefined;
101
102
  if (!fieldInterface) continue;
102
103
  if (fieldInterface === 'formula') continue;
@@ -123,23 +124,143 @@ export function buildFieldAssignCascaderOptionsFromCollection(
123
124
  function mergeRootOptions(
124
125
  configured: FieldAssignCascaderOption[],
125
126
  allFields: FieldAssignCascaderOption[],
127
+ ): FieldAssignCascaderOption[] {
128
+ return mergeCascaderOptions(configured, allFields);
129
+ }
130
+
131
+ function mergeCascaderOptions(
132
+ configured: FieldAssignCascaderOption[],
133
+ allFields: FieldAssignCascaderOption[],
126
134
  ): FieldAssignCascaderOption[] {
127
135
  const configuredList = Array.isArray(configured) ? configured : [];
128
136
  const allList = Array.isArray(allFields) ? allFields : [];
137
+ const result = configuredList.map((item) => cloneCascaderOption(item));
129
138
 
130
- const configuredValues = new Set<string>();
131
- for (const it of configuredList) {
132
- const v = it?.value ? String(it.value) : '';
133
- if (v) configuredValues.add(v);
139
+ for (const item of allList) {
140
+ mergeCascaderOptionInto(result, item);
134
141
  }
135
142
 
136
- const extra = allList.filter((it) => {
137
- const v = it?.value ? String(it.value) : '';
138
- if (!v) return false;
139
- return !configuredValues.has(v);
140
- });
143
+ return result;
144
+ }
145
+
146
+ function cloneCascaderOption(option: FieldAssignCascaderOption): FieldAssignCascaderOption {
147
+ return {
148
+ ...option,
149
+ children: option.children?.map((child) => cloneCascaderOption(child)),
150
+ };
151
+ }
152
+
153
+ function mergeCascaderOptionInto(options: FieldAssignCascaderOption[], source: FieldAssignCascaderOption) {
154
+ const value = source?.value ? String(source.value) : '';
155
+ if (!value) return;
156
+
157
+ const existing = options.find((item) => String(item?.value || '') === value);
158
+ if (!existing) {
159
+ options.push(cloneCascaderOption(source));
160
+ return;
161
+ }
162
+
163
+ if (!existing.label && source.label) {
164
+ existing.label = source.label;
165
+ }
166
+ if (source.isLeaf === false || existing.children?.length || source.children?.length) {
167
+ existing.isLeaf = false;
168
+ }
169
+ if (source.loading) {
170
+ existing.loading = source.loading;
171
+ }
172
+ if (source.children?.length) {
173
+ existing.children = mergeCascaderOptions(existing.children || [], source.children);
174
+ }
175
+ }
176
+
177
+ function getCollectionField(collection: CollectionLike | null, name: string): CollectionFieldLike | undefined {
178
+ const field = typeof collection?.getField === 'function' ? collection.getField(name) : undefined;
179
+ return field && typeof field === 'object' ? (field as CollectionFieldLike) : undefined;
180
+ }
181
+
182
+ function isAssociationFieldLike(field?: CollectionFieldLike | null) {
183
+ return !!(field?.isAssociationField?.() || field?.target || field?.targetCollection);
184
+ }
185
+
186
+ function getFieldLabelFromCollection(collection: CollectionLike | null, name: string, t: (key: string) => string) {
187
+ const field = getCollectionField(collection, name);
188
+ const title = typeof field?.title === 'string' && field.title ? field.title : name;
189
+ return t(title);
190
+ }
191
+
192
+ function getAssociationDepthFromFieldPath(
193
+ rootCollection: CollectionLike | null,
194
+ segments: string[],
195
+ ): number | undefined {
196
+ if (!rootCollection) return undefined;
197
+
198
+ let collection: CollectionLike | null = rootCollection;
199
+ let associationDepth = 0;
200
+ for (let index = 0; index < segments.length; index++) {
201
+ const field = getCollectionField(collection, segments[index]);
202
+ if (!field) return undefined;
203
+
204
+ if (isAssociationFieldLike(field)) {
205
+ associationDepth += 1;
206
+ collection = field.targetCollection || null;
207
+ continue;
208
+ }
209
+
210
+ if (index < segments.length - 1) return undefined;
211
+ }
212
+
213
+ return associationDepth;
214
+ }
215
+
216
+ function shouldIncludeConfiguredFieldPath(options: {
217
+ segments: string[];
218
+ rootCollection: CollectionLike | null;
219
+ leafField?: CollectionFieldLike | null;
220
+ maxAssociationFieldDepth: number;
221
+ }) {
222
+ const resolvedAssociationDepth = getAssociationDepthFromFieldPath(options.rootCollection, options.segments);
223
+ if (typeof resolvedAssociationDepth === 'number') {
224
+ return resolvedAssociationDepth <= options.maxAssociationFieldDepth;
225
+ }
226
+
227
+ const leafIsAssociation = isAssociationFieldLike(options.leafField);
228
+ const maxPathLength = leafIsAssociation ? options.maxAssociationFieldDepth : options.maxAssociationFieldDepth + 1;
229
+ return options.segments.length <= maxPathLength;
230
+ }
231
+
232
+ function buildNestedOptionFromFieldPath(options: {
233
+ targetPath: string;
234
+ leaf: FieldAssignCascaderOption;
235
+ rootCollection: CollectionLike | null;
236
+ t: (key: string) => string;
237
+ }): FieldAssignCascaderOption {
238
+ const segments = splitFieldPath(options.targetPath);
239
+ if (segments.length <= 1) {
240
+ return options.leaf;
241
+ }
242
+
243
+ const build = (index: number, collection: CollectionLike | null): FieldAssignCascaderOption => {
244
+ const value = segments[index];
245
+ const isLast = index === segments.length - 1;
246
+ if (isLast) {
247
+ return {
248
+ ...options.leaf,
249
+ value,
250
+ };
251
+ }
252
+
253
+ const field = getCollectionField(collection, value);
254
+ const nextCollection = field?.targetCollection || null;
255
+ return {
256
+ label: getFieldLabelFromCollection(collection, value, options.t),
257
+ value,
258
+ isLeaf: false,
259
+ children: [build(index + 1, nextCollection)],
260
+ };
261
+ };
141
262
 
142
- return [...configuredList, ...extra];
263
+ return build(0, options.rootCollection);
143
264
  }
144
265
 
145
266
  export function collectFieldAssignCascaderOptions(options: {
@@ -167,7 +288,8 @@ export function collectFieldAssignCascaderOptions(options: {
167
288
  const targetPath = getItemFieldPath(item);
168
289
  if (!targetPath) continue;
169
290
 
170
- const seg = getLastPathSegment(targetPath);
291
+ const segments = splitFieldPath(targetPath);
292
+ const seg = segments[segments.length - 1];
171
293
  if (!seg) continue;
172
294
 
173
295
  const node: FieldAssignCascaderOption = {
@@ -179,10 +301,16 @@ export function collectFieldAssignCascaderOptions(options: {
179
301
  const childItems = fieldModel?.subModels?.grid?.subModels?.items;
180
302
  const cf: (CollectionField & { target?: unknown; targetCollection?: unknown }) | undefined =
181
303
  fieldModel?.context?.collectionField;
182
- const isAssociation = !!(cf?.isAssociationField?.() || cf?.target || cf?.targetCollection);
304
+ const isAssociation = isAssociationFieldLike(cf);
183
305
  const hasTargetCollection = !!cf?.targetCollection;
184
- const associationDepth = isAssociation ? targetPath.split('.').filter(Boolean).length : 0;
185
- if (isAssociation && associationDepth > maxAssociationFieldDepth) {
306
+ if (
307
+ !shouldIncludeConfiguredFieldPath({
308
+ segments,
309
+ rootCollection,
310
+ leafField: cf,
311
+ maxAssociationFieldDepth,
312
+ })
313
+ ) {
186
314
  continue;
187
315
  }
188
316
 
@@ -203,7 +331,7 @@ export function collectFieldAssignCascaderOptions(options: {
203
331
  node.isLeaf = true;
204
332
  }
205
333
  }
206
- out.push(node);
334
+ mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
207
335
  continue;
208
336
  }
209
337
 
@@ -215,7 +343,7 @@ export function collectFieldAssignCascaderOptions(options: {
215
343
  node.isLeaf = true;
216
344
  }
217
345
 
218
- out.push(node);
346
+ mergeCascaderOptionInto(out, buildNestedOptionFromFieldPath({ targetPath, leaf: node, rootCollection, t }));
219
347
  }
220
348
 
221
349
  return out;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createSafeDocument, createSafeWindow, createSafeNavigator, tExpr } from '@nocobase/flow-engine';
10
+ import { tExpr } from '@nocobase/flow-engine';
11
11
  import type { ButtonProps } from 'antd/es/button';
12
12
  import { CodeEditor } from '../../components/code-editor';
13
13
  import { ActionModel } from '../base';
@@ -72,12 +72,7 @@ ctx.message.info('Hello JS action.');
72
72
  },
73
73
  async handler(ctx, params) {
74
74
  const { code, version } = resolveRunJsParams(ctx, params);
75
- const navigator = createSafeNavigator();
76
- await ctx.runjs(
77
- code,
78
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
79
- { version },
80
- );
75
+ await ctx.runjs(code, undefined, { version });
81
76
  },
82
77
  },
83
78
  },
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createSafeDocument, createSafeWindow, createSafeNavigator, tExpr } from '@nocobase/flow-engine';
10
+ import { tExpr } from '@nocobase/flow-engine';
11
11
  import type { ButtonProps } from 'antd/es/button';
12
12
  import { CodeEditor } from '../../components/code-editor';
13
13
  import { ActionModel, ActionSceneEnum } from '../base';
@@ -78,12 +78,7 @@ if (!rows.length) {
78
78
  },
79
79
  async handler(ctx, params) {
80
80
  const { code, version } = resolveRunJsParams(ctx, params);
81
- const navigator = createSafeNavigator();
82
- await ctx.runjs(
83
- code,
84
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
85
- { version },
86
- );
81
+ await ctx.runjs(code, undefined, { version });
87
82
  },
88
83
  },
89
84
  },
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ElementProxy, createSafeDocument, createSafeNavigator, createSafeWindow, tExpr } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React from 'react';
12
12
  import { CodeEditor } from '../../components/code-editor';
13
13
  import { ActionModel, ActionSceneEnum, CollectionActionGroupModel, RecordActionGroupModel } from '../base';
@@ -137,12 +137,7 @@ JSItemActionModel.registerFlow({
137
137
  ctx.defineProperty('element', {
138
138
  get: () => new ElementProxy(element),
139
139
  });
140
- const navigator = createSafeNavigator();
141
- await ctx.runjs(
142
- code,
143
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
144
- { version },
145
- );
140
+ await ctx.runjs(code, undefined, { version });
146
141
  });
147
142
  },
148
143
  },
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { createSafeDocument, createSafeWindow, createSafeNavigator, tExpr } from '@nocobase/flow-engine';
10
+ import { tExpr } from '@nocobase/flow-engine';
11
11
  import type { ButtonProps } from 'antd/es/button';
12
12
  import { CodeEditor } from '../../components/code-editor';
13
13
  import { ActionSceneEnum, RecordActionModel } from '../base';
@@ -78,12 +78,7 @@ if (!ctx.record) {
78
78
  },
79
79
  async handler(ctx, params) {
80
80
  const { code, version } = resolveRunJsParams(ctx, params);
81
- const navigator = createSafeNavigator();
82
- await ctx.runjs(
83
- code,
84
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
85
- { version },
86
- );
81
+ await ctx.runjs(code, undefined, { version });
87
82
  },
88
83
  },
89
84
  },