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

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 (68) 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 +2 -1
  5. package/es/flow/actions/linkageRules.d.ts +2 -0
  6. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
  8. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
  9. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  10. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  11. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  12. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +5 -0
  13. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  14. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  15. package/es/index.mjs +79 -67
  16. package/lib/index.js +80 -68
  17. package/package.json +6 -5
  18. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
  19. package/src/__tests__/settings-center.test.tsx +30 -0
  20. package/src/components/form/JsonTextArea.tsx +129 -0
  21. package/src/components/index.ts +1 -0
  22. package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
  23. package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
  24. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  25. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
  26. package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
  27. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  28. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  29. package/src/flow/actions/index.ts +3 -0
  30. package/src/flow/actions/linkageRules.tsx +194 -42
  31. package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
  32. package/src/flow/actions/openView.tsx +2 -1
  33. package/src/flow/actions/pattern.tsx +25 -2
  34. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  35. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  36. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
  37. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
  38. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
  39. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
  40. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
  41. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
  42. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
  43. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
  44. package/src/flow/components/AdminLayout.tsx +2 -2
  45. package/src/flow/components/FlowRoute.tsx +17 -4
  46. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  47. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  48. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  49. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  50. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  51. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  52. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
  53. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +34 -3
  54. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  55. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  56. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  57. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -1
  58. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  59. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  60. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  61. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  62. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  63. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  64. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  65. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  66. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  67. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
  68. package/src/flow/system-settings/useSystemSettings.tsx +36 -1
@@ -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';
@@ -41,9 +42,11 @@ export {
41
42
  detailsFieldLinkageRules,
42
43
  linkageSetDetailsFieldProps,
43
44
  actionLinkageRules,
45
+ menuLinkageRules,
44
46
  blockLinkageRules,
45
47
  linkageSetBlockProps,
46
48
  linkageSetActionProps,
49
+ linkageSetMenuItemProps,
47
50
  linkageSetFieldProps,
48
51
  subFormLinkageSetFieldProps,
49
52
  linkageAssignField,
@@ -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 {
@@ -483,6 +479,41 @@ export const linkageSetActionProps = defineAction({
483
479
  },
484
480
  });
485
481
 
482
+ export const linkageSetMenuItemProps = defineAction({
483
+ name: 'linkageSetMenuItemProps',
484
+ title: tExpr('Set menu item state'),
485
+ scene: ActionScene.MENU_LINKAGE_RULES,
486
+ sort: 100,
487
+ uiSchema: {
488
+ value: {
489
+ type: 'string',
490
+ 'x-component': (props) => {
491
+ const { value, onChange } = props;
492
+ // eslint-disable-next-line react-hooks/rules-of-hooks
493
+ const ctx = useFlowContext();
494
+ const t = ctx.model.translate.bind(ctx.model);
495
+
496
+ return (
497
+ <Select
498
+ value={value}
499
+ onChange={onChange}
500
+ placeholder={t('Please select state')}
501
+ style={{ width: '100%' }}
502
+ options={[
503
+ { label: t('Visible'), value: 'visible' },
504
+ { label: t('Hidden'), value: 'hidden' },
505
+ ]}
506
+ allowClear
507
+ />
508
+ );
509
+ },
510
+ },
511
+ },
512
+ handler(ctx, { value, setProps }) {
513
+ setProps(ctx.model, { hiddenModel: value === 'hidden' });
514
+ },
515
+ });
516
+
486
517
  export const linkageSetFieldProps = defineAction({
487
518
  name: 'linkageSetFieldProps',
488
519
  title: tExpr('Set field state'),
@@ -1292,6 +1323,7 @@ export const linkageRunjs = defineAction({
1292
1323
  ActionScene.BLOCK_LINKAGE_RULES,
1293
1324
  ActionScene.FIELD_LINKAGE_RULES,
1294
1325
  ActionScene.ACTION_LINKAGE_RULES,
1326
+ ActionScene.MENU_LINKAGE_RULES,
1295
1327
  ActionScene.DETAILS_FIELD_LINKAGE_RULES,
1296
1328
  ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
1297
1329
  ],
@@ -1759,7 +1791,9 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1759
1791
 
1760
1792
  const linkageRules: LinkageRule[] = params.value as LinkageRule[];
1761
1793
  const allModels: FlowModel[] = ctx.model.__allModels || (ctx.model.__allModels = []);
1762
- const directValuePatches: Array<{ path: any; value: any }> = [];
1794
+ const modelsToApply = new Set<FlowModel>(allModels);
1795
+ const patchPropsByModel = new Map<FlowModel, any>();
1796
+ const directValuePatches: Array<{ path: Array<string | number>; value: any; whenEmpty?: boolean }> = [];
1763
1797
  const rootCollection = getCollectionFromModel((ctx.model as any)?.context?.blockModel ?? ctx.model);
1764
1798
  const isSafeToWriteAssociationSubpath = (namePath: any): boolean => {
1765
1799
  if (!Array.isArray(namePath) || !namePath.length) return true;
@@ -1800,6 +1834,24 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1800
1834
  const fieldIndex = (ctx.model as any)?.context?.fieldIndex;
1801
1835
  return resolveDynamicNamePath(path, fieldIndex);
1802
1836
  };
1837
+ const getDefaultPatchRuntime = () => {
1838
+ const blockModel = (ctx.model as any)?.context?.blockModel ?? ctx.model;
1839
+ return blockModel?.formValueRuntime ?? (ctx as any)?.formValueRuntime;
1840
+ };
1841
+ const rememberAppliedDefaultPatches = (patches: typeof directValuePatches) => {
1842
+ const runtime = getDefaultPatchRuntime();
1843
+ if (typeof runtime?.recordDefaultValuePatch !== 'function') return;
1844
+
1845
+ const lastPatchByPathKey = new Map<string, (typeof directValuePatches)[number]>();
1846
+ for (const patch of patches) {
1847
+ lastPatchByPathKey.set(namePathToPathKey(patch.path), patch);
1848
+ }
1849
+
1850
+ for (const patch of lastPatchByPathKey.values()) {
1851
+ if (!patch.whenEmpty) continue;
1852
+ runtime.recordDefaultValuePatch(patch.path, patch.value);
1853
+ }
1854
+ };
1803
1855
  const addFormValuePatch = (patch: { path: any; value: any; whenEmpty?: boolean }) => {
1804
1856
  if (!patch) return;
1805
1857
  const path = (patch as any)?.path;
@@ -1823,20 +1875,33 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1823
1875
  return;
1824
1876
  }
1825
1877
  const whenEmpty = !!(patch as any)?.whenEmpty;
1878
+ const value = (patch as any)?.value;
1826
1879
  try {
1827
1880
  const form = ctx.model?.context?.form;
1828
1881
  const current = form?.getFieldValue?.(resolvedPath);
1829
- if (whenEmpty && typeof current !== 'undefined' && current !== null && current !== '') {
1830
- return;
1882
+ if (whenEmpty) {
1883
+ const runtime = getDefaultPatchRuntime();
1884
+ if (typeof runtime?.canApplyDefaultValuePatch === 'function') {
1885
+ const canApply = runtime.canApplyDefaultValuePatch(resolvedPath, value);
1886
+ if (!canApply) {
1887
+ return;
1888
+ }
1889
+ } else if (typeof current !== 'undefined' && current !== null && current !== '') {
1890
+ return;
1891
+ }
1831
1892
  }
1832
- if (_.isEqual(current, (patch as any)?.value)) {
1893
+ if (_.isEqual(current, value)) {
1833
1894
  return;
1834
1895
  }
1835
1896
  } catch {
1836
1897
  // ignore
1837
1898
  }
1838
1899
 
1839
- directValuePatches.push({ path: resolvedPath, value: (patch as any)?.value });
1900
+ directValuePatches.push({
1901
+ path: resolvedPath,
1902
+ value,
1903
+ ...(whenEmpty ? { whenEmpty: true } : {}),
1904
+ });
1840
1905
  };
1841
1906
 
1842
1907
  const getModelTargetPathForPatch = (model: any): string | null => {
@@ -1879,11 +1944,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1879
1944
  return null;
1880
1945
  };
1881
1946
 
1882
- allModels.forEach((model: any) => {
1883
- // 重置临时属性
1884
- model.__props = {};
1885
- });
1886
-
1887
1947
  // 1. 运行所有的联动规则
1888
1948
  for (const rule of linkageRules.filter((r) => r.enable)) {
1889
1949
  const { condition: conditions, actions } = rule;
@@ -1892,10 +1952,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1892
1952
  if (!matched) continue;
1893
1953
 
1894
1954
  for (const action of actions) {
1895
- const setProps = (
1896
- model: FlowModel & { __originalProps?: any; __props?: any; __shouldReset?: boolean },
1897
- props: any,
1898
- ) => {
1955
+ const setProps = (model: FlowModel & { __originalProps?: any; __shouldReset?: boolean }, props: any) => {
1899
1956
  // 存储原始值,用于恢复
1900
1957
  if (!model.__originalProps) {
1901
1958
  model.__originalProps = {
@@ -1908,19 +1965,16 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1908
1965
  };
1909
1966
  }
1910
1967
 
1911
- if (!model.__props) {
1912
- model.__props = {};
1913
- }
1914
-
1915
1968
  // 临时存起来,遍历完所有规则后,再统一处理
1916
- model.__props = {
1917
- ...model.__props,
1969
+ patchPropsByModel.set(model, {
1970
+ ...(patchPropsByModel.get(model) || {}),
1918
1971
  ...props,
1919
- };
1972
+ });
1920
1973
 
1921
1974
  if (allModels.indexOf(model) === -1) {
1922
1975
  allModels.push(model);
1923
1976
  }
1977
+ modelsToApply.add(model);
1924
1978
  };
1925
1979
 
1926
1980
  // TODO: 需要改成 runAction 的写法。但 runAction 是异步的,用在这里会不符合预期。后面需要解决这个问题
@@ -1929,15 +1983,12 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1929
1983
  }
1930
1984
 
1931
1985
  // 2. 合并去重(按 uid)后再实际更改相关 model 的状态,避免重复项把“已设置的临时属性”覆盖掉
1932
- const mergedByUid = new Map<
1933
- string,
1934
- FlowModel & { __originalProps?: any; __props?: any; isFork?: boolean; forkId?: number }
1935
- >();
1986
+ const mergedByUid = new Map<string, FlowModel & { __originalProps?: any; isFork?: boolean; forkId?: number }>();
1936
1987
  const mergedPropsByUid = new Map<string, any>();
1937
1988
 
1938
- allModels.forEach((m: any) => {
1989
+ modelsToApply.forEach((m: any) => {
1939
1990
  const uid = m?.uid || String(m);
1940
- const curProps = m.__props || {};
1991
+ const curProps = patchPropsByModel.get(m) || {};
1941
1992
  if (!mergedByUid.has(uid)) {
1942
1993
  mergedByUid.set(uid, m);
1943
1994
  mergedPropsByUid.set(uid, { ...curProps });
@@ -1956,7 +2007,11 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
1956
2007
  const newProps = { ...model.__originalProps, ...patchProps };
1957
2008
 
1958
2009
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText']));
1959
- model.hidden = !!newProps.hiddenModel;
2010
+ if (typeof model.setHidden === 'function') {
2011
+ model.setHidden(!!newProps.hiddenModel);
2012
+ } else {
2013
+ model.hidden = !!newProps.hiddenModel;
2014
+ }
1960
2015
 
1961
2016
  if (newProps.required === true) {
1962
2017
  const rules = (model.props.rules || []).filter((rule) => !rule.required);
@@ -2018,6 +2073,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2018
2073
  if (typeof directSetter === 'function') {
2019
2074
  try {
2020
2075
  await trySetFormValues(directSetter, directCtx, 'linkage');
2076
+ rememberAppliedDefaultPatches(allPatches);
2021
2077
  return;
2022
2078
  } catch (error) {
2023
2079
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2034,6 +2090,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2034
2090
  if (typeof blockSetter === 'function') {
2035
2091
  try {
2036
2092
  await trySetFormValues(blockSetter, blockCtx, 'linkage');
2093
+ rememberAppliedDefaultPatches(allPatches);
2037
2094
  return;
2038
2095
  } catch (error) {
2039
2096
  console.warn('[linkageRules] Failed to set form values via setFormValues', {
@@ -2063,6 +2120,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2063
2120
  console.warn('[linkageRules] Failed to set form field value (fallback setFieldValue)', { path }, error);
2064
2121
  }
2065
2122
  });
2123
+ rememberAppliedDefaultPatches(allPatches);
2066
2124
  };
2067
2125
 
2068
2126
  export const blockLinkageRules = defineAction({
@@ -2119,6 +2177,32 @@ export const actionLinkageRules = defineAction({
2119
2177
  },
2120
2178
  });
2121
2179
 
2180
+ export const menuLinkageRules = defineAction({
2181
+ name: 'menuLinkageRules',
2182
+ title: tExpr('Menu linkage rules'),
2183
+ uiMode: 'embed',
2184
+ uiSchema(ctx) {
2185
+ return {
2186
+ value: {
2187
+ type: 'array',
2188
+ 'x-component': LinkageRulesUI,
2189
+ 'x-component-props': {
2190
+ supportedActions: getSupportedActions(ctx, ActionScene.MENU_LINKAGE_RULES),
2191
+ title: tExpr('Menu linkage rules'),
2192
+ },
2193
+ },
2194
+ };
2195
+ },
2196
+ defaultParams: {
2197
+ value: [],
2198
+ },
2199
+ useRawParams: true,
2200
+ handler: async (ctx, params) => {
2201
+ const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
2202
+ return commonLinkageRulesHandler(ctx, resolved);
2203
+ },
2204
+ });
2205
+
2122
2206
  export const fieldLinkageRules = defineAction({
2123
2207
  name: 'fieldLinkageRules',
2124
2208
  title: tExpr('Field linkage rules'),
@@ -2348,33 +2432,95 @@ export const fieldLinkageRules = defineAction({
2348
2432
  return;
2349
2433
  }
2350
2434
 
2351
- const getRowScopeKeyFromModel = (model: any): string | null => {
2435
+ const getRowFieldIndexInfoFromModel = (model: any) => {
2352
2436
  const fieldIndex = model?.context?.fieldIndex;
2353
2437
  const arr = Array.isArray(fieldIndex) ? fieldIndex : [];
2354
- if (!arr.length) return null;
2438
+ const normalized = arr.filter((it): it is string => typeof it === 'string');
2355
2439
  const entries: Array<{ name: string; index: number }> = [];
2356
- for (const it of arr) {
2357
- if (typeof it !== 'string') continue;
2440
+ const path: Array<string | number> = [];
2441
+ for (const it of normalized) {
2358
2442
  const [name, indexStr] = it.split(':');
2359
2443
  const index = Number(indexStr);
2360
2444
  if (!name || Number.isNaN(index)) continue;
2361
2445
  entries.push({ name, index });
2446
+ path.push(name, index);
2362
2447
  }
2448
+ return { normalized, entries, path };
2449
+ };
2450
+
2451
+ const getRowScopeKeyFromModel = (model: any): string | null => {
2452
+ const { entries } = getRowFieldIndexInfoFromModel(model);
2363
2453
  if (!entries.length) return null;
2364
2454
  const deepest = entries[entries.length - 1].name;
2365
2455
  const occurrence = entries.reduce((count, e) => (e.name === deepest ? count + 1 : count), 0);
2366
2456
  return `${deepest}#${occurrence}`;
2367
2457
  };
2368
2458
 
2459
+ const getRowFieldIndexKeyFromModel = (model: any): string | null => {
2460
+ const { normalized } = getRowFieldIndexInfoFromModel(model);
2461
+ if (!normalized.length) return null;
2462
+ return JSON.stringify(normalized);
2463
+ };
2464
+
2465
+ const getRowPathFromModel = (model: any): Array<string | number> | null => {
2466
+ const { path } = getRowFieldIndexInfoFromModel(model);
2467
+ return path.length ? path : null;
2468
+ };
2469
+
2470
+ const getFormForRowFork = (model: any) => {
2471
+ return (
2472
+ model?.context?.form ??
2473
+ model?.context?.blockModel?.context?.form ??
2474
+ ctx.model?.context?.form ??
2475
+ ctx.model?.context?.blockModel?.context?.form
2476
+ );
2477
+ };
2478
+
2479
+ const isRowForkMountedInCurrentValue = (model: any): boolean => {
2480
+ const rowPath = getRowPathFromModel(model);
2481
+ if (!rowPath) return true;
2482
+ const form = getFormForRowFork(model);
2483
+ if (!form || typeof form.getFieldValue !== 'function') return true;
2484
+ return typeof form.getFieldValue(rowPath as any) !== 'undefined';
2485
+ };
2486
+
2487
+ const hasRowItemContext = (model: any): boolean => {
2488
+ const itemOptions = model?.context?.getPropertyOptions?.('item');
2489
+ if (itemOptions) return true;
2490
+ return typeof model?.context?.item !== 'undefined';
2491
+ };
2492
+
2493
+ const hasSubTableRowMarker = (model: any): boolean => {
2494
+ const markerOptions = model?.context?.getPropertyOptions?.('subTableRowFork');
2495
+ if (markerOptions) return true;
2496
+ return (
2497
+ typeof model?.context?.subTableRowFork !== 'undefined' ||
2498
+ typeof model?.subTableRowFork !== 'undefined' ||
2499
+ model?.subTableRowFork === true
2500
+ );
2501
+ };
2502
+
2369
2503
  const isRowGridForkModel = (model: any): boolean => {
2370
2504
  if (!model || typeof model !== 'object') return false;
2371
2505
  if ((model as any)?.subModels?.field) return false;
2372
2506
  if (!(model as any)?.subModels?.items) return false;
2373
- return !!getRowScopeKeyFromModel(model);
2507
+ return !!getRowScopeKeyFromModel(model) && !!getRowFieldIndexKeyFromModel(model);
2508
+ };
2509
+
2510
+ const isSubTableRowForkModel = (model: any): boolean => {
2511
+ if (!model || typeof model !== 'object') return false;
2512
+ if (!hasSubTableRowMarker(model)) return false;
2513
+ if (!getRowScopeKeyFromModel(model) || !getRowFieldIndexKeyFromModel(model)) return false;
2514
+ return hasRowItemContext(model);
2515
+ };
2516
+
2517
+ const isRowScopedForkModel = (model: any): boolean => {
2518
+ return isRowGridForkModel(model) || isSubTableRowForkModel(model);
2374
2519
  };
2375
2520
 
2376
- const collectRowGridForksByKey = (): Map<string, FlowModel[]> => {
2521
+ const collectRowScopedForksByKey = (): Map<string, FlowModel[]> => {
2377
2522
  const out = new Map<string, FlowModel[]>();
2523
+ const seenByKey = new Map<string, Set<string>>();
2378
2524
  const engine = ctx.engine;
2379
2525
  if (!engine?.forEachModel) return out;
2380
2526
 
@@ -2383,9 +2529,15 @@ export const fieldLinkageRules = defineAction({
2383
2529
  if (!forks || typeof forks.forEach !== 'function') return;
2384
2530
  forks.forEach((fork: any) => {
2385
2531
  if (!fork || fork.disposed) return;
2386
- if (!isRowGridForkModel(fork)) return;
2532
+ if (!isRowScopedForkModel(fork)) return;
2533
+ if (!isRowForkMountedInCurrentValue(fork)) return;
2387
2534
  const rowScopeKey = getRowScopeKeyFromModel(fork);
2388
- if (!rowScopeKey) return;
2535
+ const fieldIndexKey = getRowFieldIndexKeyFromModel(fork);
2536
+ if (!rowScopeKey || !fieldIndexKey) return;
2537
+ const seen = seenByKey.get(rowScopeKey) || new Set<string>();
2538
+ if (seen.has(fieldIndexKey)) return;
2539
+ seen.add(fieldIndexKey);
2540
+ seenByKey.set(rowScopeKey, seen);
2389
2541
  const arr = out.get(rowScopeKey) || [];
2390
2542
  arr.push(fork as FlowModel);
2391
2543
  out.set(rowScopeKey, arr);
@@ -2396,7 +2548,7 @@ export const fieldLinkageRules = defineAction({
2396
2548
  };
2397
2549
 
2398
2550
  const runRowScoped = async (): Promise<boolean> => {
2399
- const forksByKey = collectRowGridForksByKey();
2551
+ const forksByKey = collectRowScopedForksByKey();
2400
2552
  let hasAnyRowFork = false;
2401
2553
  for (const [rowScopeKey, rowParams] of rowParamsByKey.entries()) {
2402
2554
  const forks = forksByKey.get(rowScopeKey) || [];