@nocobase/client-v2 2.1.0-beta.27 → 2.1.0-beta.29

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 (45) hide show
  1. package/es/components/form/JsonTextArea.d.ts +18 -0
  2. package/es/components/index.d.ts +1 -0
  3. package/es/flow/actions/dateRangeLimit.d.ts +9 -0
  4. package/es/flow/actions/index.d.ts +1 -0
  5. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  6. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  7. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  8. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
  9. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  10. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  11. package/es/index.mjs +72 -65
  12. package/lib/index.js +61 -54
  13. package/package.json +6 -5
  14. package/src/components/form/JsonTextArea.tsx +129 -0
  15. package/src/components/index.ts +1 -0
  16. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  17. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  18. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  19. package/src/flow/actions/index.ts +1 -0
  20. package/src/flow/actions/linkageRules.tsx +117 -19
  21. package/src/flow/actions/openView.tsx +2 -1
  22. package/src/flow/actions/pattern.tsx +25 -2
  23. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  24. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  25. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  26. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  27. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  28. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  29. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  30. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  31. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
  32. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  33. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  34. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  35. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
  36. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  37. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  38. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  39. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  40. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  41. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  42. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  43. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  44. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  45. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
@@ -0,0 +1,190 @@
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
+
10
+ import { describe, expect, it, vi } from 'vitest';
11
+ import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
12
+ import { FieldModel } from '../../models/base/FieldModel';
13
+ import { JSEditableFieldModel } from '../../models/fields/JSEditableFieldModel';
14
+ import { DetailsItemModel } from '../../models/blocks/details/DetailsItemModel';
15
+ import { pattern } from '../pattern';
16
+
17
+ class DummyDisplayFieldModel extends FieldModel {}
18
+
19
+ class DummyFormItemModel extends FlowModel<{ subModels: { field?: FieldModel } }> {
20
+ collectionField: any;
21
+
22
+ static getDefaultBindingByField() {
23
+ return { modelName: 'FieldModel' };
24
+ }
25
+
26
+ getFieldSettingsInitParams() {
27
+ return { mock: true };
28
+ }
29
+ }
30
+
31
+ function makeCollectionField() {
32
+ return {
33
+ targetCollection: undefined,
34
+ isAssociationField: () => false,
35
+ };
36
+ }
37
+
38
+ function makeCtx(parent: DummyFormItemModel) {
39
+ const collectionField = makeCollectionField();
40
+ parent.collectionField = collectionField;
41
+ return {
42
+ model: parent,
43
+ collectionField,
44
+ engine: parent.flowEngine,
45
+ } as any;
46
+ }
47
+
48
+ describe('pattern action', () => {
49
+ it('keeps JS editable field model when switching to display only', async () => {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({
52
+ DummyFormItemModel,
53
+ FieldModel,
54
+ JSEditableFieldModel,
55
+ DummyDisplayFieldModel,
56
+ });
57
+
58
+ const parent = engine.createModel<DummyFormItemModel>({
59
+ use: DummyFormItemModel,
60
+ uid: 'form-item-js',
61
+ subModels: {
62
+ field: {
63
+ use: FieldModel,
64
+ uid: 'field-js',
65
+ stepParams: {
66
+ fieldBinding: {
67
+ use: 'JSEditableFieldModel',
68
+ },
69
+ jsSettings: {
70
+ runJs: {
71
+ code: 'ctx.render("hello")',
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ });
78
+ const field = parent.subModels.field;
79
+ const saveSpy = vi.spyOn(engine, 'saveModel');
80
+ const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
81
+
82
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'editable' });
83
+
84
+ expect(parent.subModels.field).toBe(field);
85
+ expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
86
+ expect(parent.subModels.field?.uid).toBe('field-js');
87
+ expect(parent.subModels.field?.getStepParams('jsSettings', 'runJs')).toMatchObject({
88
+ code: 'ctx.render("hello")',
89
+ });
90
+ expect(saveSpy).not.toHaveBeenCalled();
91
+ expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it('keeps JS editable field model when leaving display only', async () => {
95
+ const engine = new FlowEngine();
96
+ engine.registerModels({
97
+ DummyFormItemModel,
98
+ FieldModel,
99
+ JSEditableFieldModel,
100
+ DummyDisplayFieldModel,
101
+ });
102
+
103
+ const parent = engine.createModel<DummyFormItemModel>({
104
+ use: DummyFormItemModel,
105
+ uid: 'form-item-js-leave',
106
+ subModels: {
107
+ field: {
108
+ use: FieldModel,
109
+ uid: 'field-js-leave',
110
+ stepParams: {
111
+ fieldBinding: {
112
+ use: 'JSEditableFieldModel',
113
+ },
114
+ },
115
+ },
116
+ },
117
+ });
118
+ const field = parent.subModels.field;
119
+ const saveSpy = vi.spyOn(engine, 'saveModel');
120
+ const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
121
+
122
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'editable' }, { pattern: 'readPretty' });
123
+
124
+ expect(parent.subModels.field).toBe(field);
125
+ expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
126
+ expect(saveSpy).not.toHaveBeenCalled();
127
+ expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
128
+ });
129
+
130
+ it('does not reapply JS settings when pattern is unchanged', async () => {
131
+ const engine = new FlowEngine();
132
+ engine.registerModels({
133
+ DummyFormItemModel,
134
+ FieldModel,
135
+ JSEditableFieldModel,
136
+ });
137
+
138
+ const parent = engine.createModel<DummyFormItemModel>({
139
+ use: DummyFormItemModel,
140
+ uid: 'form-item-js-unchanged',
141
+ subModels: {
142
+ field: {
143
+ use: FieldModel,
144
+ uid: 'field-js-unchanged',
145
+ stepParams: {
146
+ fieldBinding: {
147
+ use: 'JSEditableFieldModel',
148
+ },
149
+ },
150
+ },
151
+ },
152
+ });
153
+ const applyJsSettingsSpy = vi.spyOn(parent.subModels.field as JSEditableFieldModel, 'scheduleApplyJsSettings');
154
+
155
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'readPretty' });
156
+
157
+ expect(applyJsSettingsSpy).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it('still rebuilds regular fields when switching to display only', async () => {
161
+ const engine = new FlowEngine();
162
+ engine.registerModels({
163
+ DummyFormItemModel,
164
+ FieldModel,
165
+ DummyDisplayFieldModel,
166
+ });
167
+
168
+ const parent = engine.createModel<DummyFormItemModel>({
169
+ use: DummyFormItemModel,
170
+ uid: 'form-item-regular',
171
+ subModels: {
172
+ field: {
173
+ use: FieldModel,
174
+ uid: 'field-regular',
175
+ },
176
+ },
177
+ });
178
+ const ctx = makeCtx(parent);
179
+ const displayBinding = { modelName: 'DummyDisplayFieldModel', defaultProps: { display: true } } as any;
180
+ const getDisplayBindingSpy = vi.spyOn(DetailsItemModel, 'getDefaultBindingByField').mockReturnValue(displayBinding);
181
+
182
+ await pattern.afterParamsSave?.(ctx, { pattern: 'readPretty' }, { pattern: 'editable' });
183
+
184
+ expect(parent.subModels.field).toBeInstanceOf(DummyDisplayFieldModel);
185
+ expect(parent.subModels.field?.uid).toBe('field-regular');
186
+ expect(parent.subModels.field?.props).toMatchObject({ display: true });
187
+
188
+ getDisplayBindingSpy.mockRestore();
189
+ });
190
+ });
@@ -0,0 +1,66 @@
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
+
10
+ import { defineAction, tExpr } from '@nocobase/flow-engine';
11
+ import { FieldAssignValueInput } from '../components/FieldAssignValueInput';
12
+
13
+ function normalizeDateRangeValue(value: any) {
14
+ if (value === '' || value === null || typeof value === 'undefined') {
15
+ return undefined;
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export const dateRangeLimit = defineAction({
21
+ name: 'dateRangeLimit',
22
+ title: tExpr('Date range limit'),
23
+ uiMode: {
24
+ type: 'dialog',
25
+ props: {
26
+ width: 720,
27
+ },
28
+ },
29
+ uiSchema(ctx: any) {
30
+ const targetPath = ctx.model.context?.collectionField?.name || '';
31
+ const dateLimitInputProps = {
32
+ targetPath,
33
+ enableDateVariableAsConstant: true,
34
+ };
35
+
36
+ return {
37
+ _minDate: {
38
+ type: 'string',
39
+ title: tExpr('MinDate'),
40
+ 'x-decorator': 'FormItem',
41
+ 'x-component': FieldAssignValueInput,
42
+ 'x-component-props': dateLimitInputProps,
43
+ },
44
+ _maxDate: {
45
+ type: 'string',
46
+ title: tExpr('MaxDate'),
47
+ 'x-decorator': 'FormItem',
48
+ 'x-component': FieldAssignValueInput,
49
+ 'x-component-props': dateLimitInputProps,
50
+ },
51
+ };
52
+ },
53
+ defaultParams(ctx: any) {
54
+ return {
55
+ _minDate: normalizeDateRangeValue(ctx.model.props?._minDate),
56
+ _maxDate: normalizeDateRangeValue(ctx.model.props?._maxDate),
57
+ };
58
+ },
59
+ useRawParams: true,
60
+ async handler(ctx: any, params) {
61
+ ctx.model.setProps({
62
+ _minDate: normalizeDateRangeValue(params?._minDate),
63
+ _maxDate: normalizeDateRangeValue(params?._maxDate),
64
+ });
65
+ },
66
+ });
@@ -20,6 +20,7 @@ export * from './refreshTargetBlocks';
20
20
  export * from './setTargetDataScope';
21
21
  export { titleField } from './titleField';
22
22
  export * from './dateTimeFormat';
23
+ export * from './dateRangeLimit';
23
24
  export * from './sortingRules';
24
25
  export * from './dataLoadingMode';
25
26
  export * from './renderMode';
@@ -51,11 +51,7 @@ import {
51
51
  getCollectionFromModel,
52
52
  isToManyAssociationField,
53
53
  } from '../internal/utils/modelUtils';
54
- import {
55
- namePathToPathKey,
56
- parsePathString,
57
- resolveDynamicNamePath,
58
- } from '../models/blocks/form/value-runtime/path';
54
+ import { namePathToPathKey, parsePathString, resolveDynamicNamePath } from '../models/blocks/form/value-runtime/path';
59
55
  import { ensureFormValueDrivenLinkageRefresh } from './linkageRulesFormValueRefresh';
60
56
 
61
57
  interface LinkageRule {
@@ -1759,7 +1755,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1759
1755
 
1760
1756
  const linkageRules: LinkageRule[] = params.value as LinkageRule[];
1761
1757
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1762
- const directValuePatches: Array<{ path: any; value: any }> = [];
1758
+ const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1763
1759
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1764
1760
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
1765
1761
  if (!Array.isArray(namePath) || !namePath.length) return true;
@@ -1800,6 +1796,24 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1800
1796
  const fieldIndex = (ctx.model as any)?.context?.fieldIndex;
1801
1797
  return resolveDynamicNamePath(path, fieldIndex);
1802
1798
  };
1799
+ const getDefaultPatchRuntime = () => {
1800
+ const blockModel = (ctx.model as any)?.context?.blockModel ?? ctx.model;
1801
+ return blockModel?.formValueRuntime ?? (ctx as any)?.formValueRuntime;
1802
+ };
1803
+ const rememberAppliedDefaultPatches = (patches: typeof directValuePatches) => {
1804
+ const runtime = getDefaultPatchRuntime();
1805
+ if (typeof runtime?.recordDefaultValuePatch !== 'function') return;
1806
+
1807
+ const lastPatchByPathKey = new Map<string, (typeof directValuePatches)[number]>();
1808
+ for (const patch of patches) {
1809
+ lastPatchByPathKey.set(namePathToPathKey(patch.path), patch);
1810
+ }
1811
+
1812
+ for (const patch of lastPatchByPathKey.values()) {
1813
+ if (!patch.whenEmpty) continue;
1814
+ runtime.recordDefaultValuePatch(patch.path, patch.value);
1815
+ }
1816
+ };
1803
1817
  const addFormValuePatch = (patch: { path: any; value: any; whenEmpty?: boolean }) => {
1804
1818
  if (!patch) return;
1805
1819
  const path = (patch as any)?.path;
@@ -1823,20 +1837,33 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1823
1837
  return;
1824
1838
  }
1825
1839
  const whenEmpty = !!(patch as any)?.whenEmpty;
1840
+ const value = (patch as any)?.value;
1826
1841
  try {
1827
1842
  const form = ctx.model?.context?.form;
1828
1843
  const current = form?.getFieldValue?.(resolvedPath);
1829
- if (whenEmpty && typeof current !== 'undefined' && current !== null && current !== '') {
1830
- return;
1844
+ if (whenEmpty) {
1845
+ const runtime = getDefaultPatchRuntime();
1846
+ if (typeof runtime?.canApplyDefaultValuePatch === 'function') {
1847
+ const canApply = runtime.canApplyDefaultValuePatch(resolvedPath, value);
1848
+ if (!canApply) {
1849
+ return;
1850
+ }
1851
+ } else if (typeof current !== 'undefined' && current !== null && current !== '') {
1852
+ return;
1853
+ }
1831
1854
  }
1832
- if (_.isEqual(current, (patch as any)?.value)) {
1855
+ if (_.isEqual(current, value)) {
1833
1856
  return;
1834
1857
  }
1835
1858
  } catch {
1836
1859
  // ignore
1837
1860
  }
1838
1861
 
1839
- directValuePatches.push({ path: resolvedPath, value: (patch as any)?.value });
1862
+ directValuePatches.push({
1863
+ path: resolvedPath,
1864
+ value,
1865
+ ...(whenEmpty ? { whenEmpty: true } : {}),
1866
+ });
1840
1867
  };
1841
1868
 
1842
1869
  const getModelTargetPathForPatch = (model: any): string | null => {
@@ -2018,6 +2045,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2018
2045
  if (typeof directSetter === 'function') {
2019
2046
  try {
2020
2047
  await trySetFormValues(directSetter, directCtx, 'linkage');
2048
+ rememberAppliedDefaultPatches(allPatches);
2021
2049
  return;
2022
2050
  } catch (error) {
2023
2051
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2034,6 +2062,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2034
2062
  if (typeof blockSetter === 'function') {
2035
2063
  try {
2036
2064
  await trySetFormValues(blockSetter, blockCtx, 'linkage');
2065
+ rememberAppliedDefaultPatches(allPatches);
2037
2066
  return;
2038
2067
  } catch (error) {
2039
2068
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2063,6 +2092,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2063
2092
  console.warn('[linkageRules] Failed to set form field value (fallback setFieldValue)', { path }, error);
2064
2093
  }
2065
2094
  });
2095
+ rememberAppliedDefaultPatches(allPatches);
2066
2096
  };
2067
2097
 
2068
2098
  export const blockLinkageRules = defineAction({
@@ -2348,33 +2378,95 @@ export const fieldLinkageRules = defineAction({
2348
2378
  return;
2349
2379
  }
2350
2380
 
2351
- const getRowScopeKeyFromModel = (model: any): string | null => {
2381
+ const getRowFieldIndexInfoFromModel = (model: any) => {
2352
2382
  const fieldIndex = model?.context?.fieldIndex;
2353
2383
  const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
2354
- if (!arr.length) return null;
2384
+ const normalized = arr.filter((it): it is string => typeof it === 'string');
2355
2385
  const entries: Array<{ name: string; index: number }> = [];
2356
- for (const it of arr) {
2357
- if (typeof it !== 'string') continue;
2386
+ const path: Array<string | number> = [];
2387
+ for (const it of normalized) {
2358
2388
  const [name, indexStr] = it.split(':');
2359
2389
  const index = Number(indexStr);
2360
2390
  if (!name || Number.isNaN(index)) continue;
2361
2391
  entries.push({ name, index });
2392
+ path.push(name, index);
2362
2393
  }
2394
+ return { normalized, entries, path };
2395
+ };
2396
+
2397
+ const getRowScopeKeyFromModel = (model: any): string | null => {
2398
+ const { entries } = getRowFieldIndexInfoFromModel(model);
2363
2399
  if (!entries.length) return null;
2364
2400
  const deepest = entries[entries.length - 1].name;
2365
2401
  const occurrence = entries.reduce((count, e) => (e.name === deepest ? count + 1 : count), 0);
2366
2402
  return `${deepest}#${occurrence}`;
2367
2403
  };
2368
2404
 
2405
+ const getRowFieldIndexKeyFromModel = (model: any): string | null => {
2406
+ const { normalized } = getRowFieldIndexInfoFromModel(model);
2407
+ if (!normalized.length) return null;
2408
+ return JSON.stringify(normalized);
2409
+ };
2410
+
2411
+ const getRowPathFromModel = (model: any): Array<string | number> | null => {
2412
+ const { path } = getRowFieldIndexInfoFromModel(model);
2413
+ return path.length ? path : null;
2414
+ };
2415
+
2416
+ const getFormForRowFork = (model: any) => {
2417
+ return (
2418
+ model?.context?.form ??
2419
+ model?.context?.blockModel?.context?.form ??
2420
+ ctx.model?.context?.form ??
2421
+ ctx.model?.context?.blockModel?.context?.form
2422
+ );
2423
+ };
2424
+
2425
+ const isRowForkMountedInCurrentValue = (model: any): boolean => {
2426
+ const rowPath = getRowPathFromModel(model);
2427
+ if (!rowPath) return true;
2428
+ const form = getFormForRowFork(model);
2429
+ if (!form || typeof form.getFieldValue !== 'function') return true;
2430
+ return typeof form.getFieldValue(rowPath as any) !== 'undefined';
2431
+ };
2432
+
2433
+ const hasRowItemContext = (model: any): boolean => {
2434
+ const itemOptions = model?.context?.getPropertyOptions?.('item');
2435
+ if (itemOptions) return true;
2436
+ return typeof model?.context?.item !== 'undefined';
2437
+ };
2438
+
2439
+ const hasSubTableRowMarker = (model: any): boolean => {
2440
+ const markerOptions = model?.context?.getPropertyOptions?.('subTableRowFork');
2441
+ if (markerOptions) return true;
2442
+ return (
2443
+ typeof model?.context?.subTableRowFork !== 'undefined' ||
2444
+ typeof model?.subTableRowFork !== 'undefined' ||
2445
+ model?.subTableRowFork === true
2446
+ );
2447
+ };
2448
+
2369
2449
  const isRowGridForkModel = (model: any): boolean => {
2370
2450
  if (!model || typeof model !== 'object') return false;
2371
2451
  if ((model as any)?.subModels?.field) return false;
2372
2452
  if (!(model as any)?.subModels?.items) return false;
2373
- return !!getRowScopeKeyFromModel(model);
2453
+ return !!getRowScopeKeyFromModel(model) && !!getRowFieldIndexKeyFromModel(model);
2454
+ };
2455
+
2456
+ const isSubTableRowForkModel = (model: any): boolean => {
2457
+ if (!model || typeof model !== 'object') return false;
2458
+ if (!hasSubTableRowMarker(model)) return false;
2459
+ if (!getRowScopeKeyFromModel(model) || !getRowFieldIndexKeyFromModel(model)) return false;
2460
+ return hasRowItemContext(model);
2461
+ };
2462
+
2463
+ const isRowScopedForkModel = (model: any): boolean => {
2464
+ return isRowGridForkModel(model) || isSubTableRowForkModel(model);
2374
2465
  };
2375
2466
 
2376
- const collectRowGridForksByKey = (): Map<string, FlowModel[]> => {
2467
+ const collectRowScopedForksByKey = (): Map<string, FlowModel[]> => {
2377
2468
  const out = new Map<string, FlowModel[]>();
2469
+ const seenByKey = new Map<string, Set<string>>();
2378
2470
  const engine = ctx.engine;
2379
2471
  if (!engine?.forEachModel) return out;
2380
2472
 
@@ -2383,9 +2475,15 @@ export const fieldLinkageRules = defineAction({
2383
2475
  if (!forks || typeof forks.forEach !== 'function') return;
2384
2476
  forks.forEach((fork: any) => {
2385
2477
  if (!fork || fork.disposed) return;
2386
- if (!isRowGridForkModel(fork)) return;
2478
+ if (!isRowScopedForkModel(fork)) return;
2479
+ if (!isRowForkMountedInCurrentValue(fork)) return;
2387
2480
  const rowScopeKey = getRowScopeKeyFromModel(fork);
2388
- if (!rowScopeKey) return;
2481
+ const fieldIndexKey = getRowFieldIndexKeyFromModel(fork);
2482
+ if (!rowScopeKey || !fieldIndexKey) return;
2483
+ const seen = seenByKey.get(rowScopeKey) || new Set<string>();
2484
+ if (seen.has(fieldIndexKey)) return;
2485
+ seen.add(fieldIndexKey);
2486
+ seenByKey.set(rowScopeKey, seen);
2389
2487
  const arr = out.get(rowScopeKey) || [];
2390
2488
  arr.push(fork as FlowModel);
2391
2489
  out.set(rowScopeKey, arr);
@@ -2396,7 +2494,7 @@ export const fieldLinkageRules = defineAction({
2396
2494
  };
2397
2495
 
2398
2496
  const runRowScoped = async (): Promise<boolean> => {
2399
- const forksByKey = collectRowGridForksByKey();
2497
+ const forksByKey = collectRowScopedForksByKey();
2400
2498
  let hasAnyRowFork = false;
2401
2499
  for (const [rowScopeKey, rowParams] of rowParamsByKey.entries()) {
2402
2500
  const forks = forksByKey.get(rowScopeKey) || [];
@@ -345,7 +345,8 @@ export const openView = defineAction({
345
345
  target: ctx.inputArgs.target || ctx.layoutContentElement,
346
346
  dataSourceKey: runtimeDataSourceKey ?? actionDefaults.dataSourceKey,
347
347
  collectionName: runtimeCollectionName ?? actionDefaults.collectionName,
348
- associationName: runtimeAssociationName ?? actionDefaults.associationName,
348
+ associationName:
349
+ typeof runtimeAssociationName !== 'undefined' ? runtimeAssociationName : actionDefaults.associationName,
349
350
  filterByTk: mergedFilterByTk,
350
351
  sourceId: mergedSourceId,
351
352
  tabUid: mergedTabUid,
@@ -7,9 +7,25 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { BindingOptions, defineAction, tExpr, DisplayItemModel } from '@nocobase/flow-engine';
10
+ import { defineAction, tExpr } from '@nocobase/flow-engine';
11
11
  import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
12
- import { rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
12
+ import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
13
+
14
+ type PatternAwareFieldModelMeta = {
15
+ preserveOnPatternChange?: boolean;
16
+ };
17
+
18
+ type PatternAwareFieldModel = {
19
+ scheduleApplyJsSettings?: () => void;
20
+ };
21
+
22
+ function shouldPreserveFieldModelOnPatternChange(ctx: any) {
23
+ const fieldModel = ctx.model.subModels.field;
24
+ const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
25
+ const ModelClass = typeof fieldUse === 'string' ? ctx.engine.getModelClass(fieldUse) : fieldUse;
26
+
27
+ return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
28
+ }
13
29
 
14
30
  export const pattern = defineAction({
15
31
  name: 'pattern',
@@ -56,6 +72,13 @@ export const pattern = defineAction({
56
72
  };
57
73
  },
58
74
  afterParamsSave: async (ctx: any, params, previousParams) => {
75
+ if (shouldPreserveFieldModelOnPatternChange(ctx)) {
76
+ if (params.pattern !== previousParams.pattern) {
77
+ (ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
78
+ }
79
+ return;
80
+ }
81
+
59
82
  const targetCollection = ctx.collectionField.targetCollection;
60
83
  const targetCollectionTitleField = targetCollection?.getField(
61
84
  ctx.model.subModels.field.props?.fieldNames?.label || ctx.model.props.titleField,
@@ -47,6 +47,8 @@ interface RouteLike {
47
47
  pathname?: string;
48
48
  }
49
49
 
50
+ const hasUsableSourceId = (sourceId: unknown) => sourceId !== undefined && sourceId !== null && String(sourceId) !== '';
51
+
50
52
  /**
51
53
  * 管理 admin 场景下每个 page 的 v2 视图栈编排。
52
54
  * 该协调器只负责状态机和开关视图,不直接绑定 React 生命周期。
@@ -264,6 +266,10 @@ export class AdminLayoutRouteCoordinator {
264
266
  const destroyRef = React.createRef<(result?: any, force?: boolean) => void>();
265
267
  const updateRef = React.createRef<(value: any) => void>();
266
268
  const openViewParams = getOpenViewStepParams(viewItem.model);
269
+ const associationName =
270
+ openViewParams?.associationName && !hasUsableSourceId(viewItem.params.sourceId)
271
+ ? null
272
+ : openViewParams?.associationName;
267
273
  const openerUids = viewList.slice(0, viewItem.index).map((item) => item.params.viewUid);
268
274
  const navigation = new ViewNavigation(
269
275
  this.flowEngine.context,
@@ -273,7 +279,7 @@ export class AdminLayoutRouteCoordinator {
273
279
  viewItem.model.dispatchEvent('click', {
274
280
  target: runtime.meta.layoutContentElement || this.layoutContentElement,
275
281
  collectionName: openViewParams?.collectionName,
276
- associationName: openViewParams?.associationName,
282
+ associationName,
277
283
  dataSourceKey: openViewParams?.dataSourceKey,
278
284
  destroyRef,
279
285
  updateRef,