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

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 (109) hide show
  1. package/es/RouteRepository.d.ts +21 -6
  2. package/es/components/form/JsonTextArea.d.ts +2 -1
  3. package/es/components/form/VariableJsonTextArea.d.ts +19 -0
  4. package/es/components/form/index.d.ts +1 -0
  5. package/es/components/index.d.ts +1 -0
  6. package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
  7. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
  8. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
  9. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
  10. package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
  11. package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
  12. package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
  13. package/es/flow/models/base/GridModel.d.ts +1 -1
  14. package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
  15. package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
  16. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
  17. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
  18. package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
  19. package/es/flow-compat/FieldValidation.d.ts +2 -1
  20. package/es/index.d.ts +2 -1
  21. package/es/index.mjs +143 -117
  22. package/lib/index.js +148 -122
  23. package/package.json +7 -7
  24. package/src/RouteRepository.ts +126 -24
  25. package/src/__tests__/RouteRepository.test.ts +216 -0
  26. package/src/__tests__/exports.test.ts +16 -0
  27. package/src/__tests__/settings-center.test.tsx +33 -0
  28. package/src/components/form/JsonTextArea.tsx +21 -11
  29. package/src/components/form/VariableJsonTextArea.tsx +175 -0
  30. package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
  31. package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
  32. package/src/components/form/index.tsx +1 -0
  33. package/src/components/index.ts +1 -0
  34. package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
  35. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
  36. package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
  37. package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
  38. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
  39. package/src/flow/actions/customVariable.tsx +1 -2
  40. package/src/flow/actions/linkageRules.tsx +122 -34
  41. package/src/flow/actions/runjs.tsx +2 -14
  42. package/src/flow/actions/validation.tsx +62 -30
  43. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
  44. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
  45. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
  46. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
  47. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
  48. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
  49. package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
  50. package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
  51. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
  52. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
  53. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
  54. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
  55. package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
  56. package/src/flow/components/DefaultValue.tsx +1 -2
  57. package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
  58. package/src/flow/components/FlowRoute.tsx +56 -11
  59. package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
  60. package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
  61. package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
  62. package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
  63. package/src/flow/models/actions/JSActionModel.tsx +2 -7
  64. package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
  65. package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
  66. package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
  67. package/src/flow/models/base/ActionModel.tsx +12 -1
  68. package/src/flow/models/base/GridModel.tsx +38 -7
  69. package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
  70. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
  71. package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
  72. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
  73. package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
  74. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
  75. package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
  76. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
  77. package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
  78. package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
  79. package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
  80. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
  81. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
  82. package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
  83. package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
  84. package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
  85. package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
  86. package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
  87. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
  88. package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
  89. package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
  90. package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
  91. package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
  92. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
  93. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
  94. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
  95. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
  96. package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
  97. package/src/flow/models/fields/InputFieldModel.tsx +14 -22
  98. package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
  99. package/src/flow/models/fields/JSFieldModel.tsx +2 -7
  100. package/src/flow/models/fields/JSItemModel.tsx +2 -14
  101. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
  102. package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
  103. package/src/flow-compat/FieldValidation.tsx +122 -60
  104. package/src/flow-compat/Popover.tsx +43 -4
  105. package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
  106. package/src/index.ts +8 -1
  107. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
  108. package/src/settings-center/AdminSettingsLayout.tsx +3 -0
  109. package/src/settings-center/plugin-manager/index.tsx +3 -0
@@ -44,6 +44,7 @@ export class FormValueRuntime {
44
44
 
45
45
  private readonly valuesMirror = observable({});
46
46
  private readonly explicitSet = new Set<string>();
47
+ private readonly userEditedSet = new Set<string>();
47
48
  private readonly lastDefaultValueByPathKey = new Map<string, any>();
48
49
  private readonly lastWriteMetaByPathKey = new Map<string, FormValueWriteMeta>();
49
50
  private readonly observableBindings = new Map<string, ObservableBinding>();
@@ -59,6 +60,8 @@ export class FormValueRuntime {
59
60
  private lastObservedToken = 0;
60
61
 
61
62
  private readonly formValuesProxy: any;
63
+ private readonly mountedFieldModelsByPathKey = new Map<string, Set<FlowModel>>();
64
+ private readonly mountedFieldModelPathKeys = new WeakMap<FlowModel, Set<string>>();
62
65
 
63
66
  private mountedListener?: (payload: { model: FlowModel }) => void;
64
67
  private unmountedListener?: (payload: { model: FlowModel }) => void;
@@ -99,6 +102,7 @@ export class FormValueRuntime {
99
102
  getFormValueAtPath: (namePath) => this.getFormValueAtPath(namePath),
100
103
  setFormValues: (callerCtx, patch, ruleOptions) => this.setFormValues(callerCtx, patch, ruleOptions),
101
104
  findExplicitHit: (pathKey) => this.findExplicitHit(pathKey),
105
+ findUserEditedHit: (pathKey) => this.findUserEditedHit(pathKey),
102
106
  lastDefaultValueByPathKey: this.lastDefaultValueByPathKey,
103
107
  lastWriteMetaByPathKey: this.lastWriteMetaByPathKey,
104
108
  observableBindings: this.observableBindings,
@@ -117,6 +121,7 @@ export class FormValueRuntime {
117
121
  *
118
122
  * - mode=default → source=default(遵循 explicit/空值覆盖语义)
119
123
  * - mode=assign → source=system(不受 explicit 影响,依赖变化时持续生效)
124
+ * - mode=override → source=override(首次覆盖已有值,用户修改后停止)
120
125
  */
121
126
  syncAssignRules(items: FormAssignRuleItem[]) {
122
127
  this.ruleEngine.syncAssignRules(items);
@@ -137,6 +142,15 @@ export class FormValueRuntime {
137
142
  return this.getForm().getFieldsValue(true);
138
143
  }
139
144
 
145
+ private toMirrorSnapshot(value: any) {
146
+ const raw = isObservable(value) ? toJS(value) : value;
147
+ return _.cloneDeepWith(raw, (item) => {
148
+ if (!item || typeof item !== 'object') return undefined;
149
+ if (Array.isArray(item) || _.isPlainObject(item)) return undefined;
150
+ return item;
151
+ });
152
+ }
153
+
140
154
  canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
141
155
  if (!namePath?.length) return false;
142
156
  if (typeof resolved === 'undefined') return false;
@@ -153,19 +167,24 @@ export class FormValueRuntime {
153
167
 
154
168
  const canOverwrite = isEmptyValue(current) || currentEqualsLastDefault;
155
169
  if (!canOverwrite && _.isEqual(current, nextSnapshot)) {
156
- this.lastDefaultValueByPathKey.set(pathKey, nextSnapshot);
170
+ this.lastDefaultValueByPathKey.set(pathKey, this.toMirrorSnapshot(nextSnapshot));
157
171
  return false;
158
172
  }
159
173
 
160
174
  return canOverwrite;
161
175
  }
162
176
 
177
+ canApplyOverrideValuePatch(namePath: NamePath) {
178
+ if (!namePath?.length) return false;
179
+ return !this.findUserEditedHit(namePathToPathKey(namePath));
180
+ }
181
+
163
182
  recordDefaultValuePatch(namePath: NamePath, value?: any) {
164
183
  if (!namePath?.length) return;
165
184
  const pathKey = namePathToPathKey(namePath);
166
185
  const snapshot =
167
186
  arguments.length >= 2 ? (isObservable(value) ? toJS(value) : value) : this.getFormValueAtPath(namePath);
168
- this.lastDefaultValueByPathKey.set(pathKey, snapshot);
187
+ this.lastDefaultValueByPathKey.set(pathKey, this.toMirrorSnapshot(snapshot));
169
188
  const current = this.getFormValueAtPath(namePath);
170
189
  const currentSnapshot = isObservable(current) ? toJS(current) : current;
171
190
  if (_.isEqual(currentSnapshot, snapshot)) {
@@ -195,9 +214,11 @@ export class FormValueRuntime {
195
214
 
196
215
  if (!this.mountedListener && !this.unmountedListener) {
197
216
  this.mountedListener = ({ model }: { model: FlowModel }) => {
217
+ this.indexMountedFieldModelTree(model);
198
218
  this.ruleEngine.onModelMounted(model);
199
219
  };
200
220
  this.unmountedListener = ({ model }: { model: FlowModel }) => {
221
+ this.unindexMountedFieldModelTree(model);
201
222
  this.ruleEngine.onModelUnmounted(model);
202
223
  };
203
224
 
@@ -205,10 +226,12 @@ export class FormValueRuntime {
205
226
  engineEmitter.on('model:unmounted', this.unmountedListener);
206
227
  }
207
228
 
229
+ this.rebuildMountedFieldModelIndex();
230
+
208
231
  if (options?.sync) {
209
232
  const snapshot = this.getFormValuesSnapshot();
210
233
  if (snapshot && typeof snapshot === 'object') {
211
- _.merge(this.valuesMirror, snapshot);
234
+ _.merge(this.valuesMirror, this.toMirrorSnapshot(snapshot));
212
235
  this.bumpChangeTick();
213
236
  }
214
237
  this.ruleEngine.enable();
@@ -228,6 +251,7 @@ export class FormValueRuntime {
228
251
  }
229
252
 
230
253
  this.ruleEngine.dispose();
254
+ this.mountedFieldModelsByPathKey.clear();
231
255
 
232
256
  for (const binding of this.observableBindings.values()) {
233
257
  binding.dispose();
@@ -241,6 +265,7 @@ export class FormValueRuntime {
241
265
  if (this.disposed) return;
242
266
 
243
267
  this.explicitSet.clear();
268
+ this.userEditedSet.clear();
244
269
  this.lastDefaultValueByPathKey.clear();
245
270
  this.lastWriteMetaByPathKey.clear();
246
271
  this.txWriteCounts.clear();
@@ -258,7 +283,7 @@ export class FormValueRuntime {
258
283
  delete (this.valuesMirror as Record<string, any>)[key];
259
284
  }
260
285
  if (snapshot && typeof snapshot === 'object') {
261
- _.merge(this.valuesMirror, snapshot);
286
+ _.merge(this.valuesMirror, this.toMirrorSnapshot(snapshot));
262
287
  }
263
288
 
264
289
  this.writeSeq += 1;
@@ -266,6 +291,13 @@ export class FormValueRuntime {
266
291
  this.ruleEngine.rescheduleAllRules();
267
292
  }
268
293
 
294
+ resetUserEditedState() {
295
+ if (this.disposed) return;
296
+
297
+ this.userEditedSet.clear();
298
+ this.ruleEngine.rescheduleAllRules();
299
+ }
300
+
269
301
  isSuppressed() {
270
302
  return this.suppressFormCallbackDepth > 0;
271
303
  }
@@ -292,7 +324,7 @@ export class FormValueRuntime {
292
324
  this.writeSeq += 1;
293
325
  bumpedWriteSeq = true;
294
326
  }
295
- _.set(this.valuesMirror, namePath, nextValue);
327
+ _.set(this.valuesMirror, namePath, this.toMirrorSnapshot(nextValue));
296
328
  changedPaths.push(namePath);
297
329
  const isMeaningfulTouched =
298
330
  field?.touched === true && !this.shouldIgnoreSyntheticTouchedInit(namePath, prevValue, nextValue);
@@ -311,6 +343,7 @@ export class FormValueRuntime {
311
343
  if (!suppressed && touchedChangedPathKeys.size) {
312
344
  for (const key of touchedChangedPathKeys) {
313
345
  this.markExplicit(key);
346
+ this.markUserEdited(key);
314
347
  }
315
348
  }
316
349
 
@@ -374,6 +407,11 @@ export class FormValueRuntime {
374
407
  this.explicitSet.delete(key);
375
408
  }
376
409
 
410
+ for (const key of Array.from(this.userEditedSet)) {
411
+ if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
412
+ this.userEditedSet.delete(key);
413
+ }
414
+
377
415
  for (const key of Array.from(this.lastDefaultValueByPathKey.keys())) {
378
416
  if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
379
417
  this.lastDefaultValueByPathKey.delete(key);
@@ -444,6 +482,7 @@ export class FormValueRuntime {
444
482
  if (!nextIndexByIdentity.size) continue;
445
483
 
446
484
  this.reconcileArrayItemSet(this.explicitSet, path, prevValue, nextIndexByIdentity);
485
+ this.reconcileArrayItemSet(this.userEditedSet, path, prevValue, nextIndexByIdentity);
447
486
  this.reconcileArrayItemMap(this.lastDefaultValueByPathKey, path, prevValue, nextIndexByIdentity);
448
487
  this.reconcileArrayItemMap(this.lastWriteMetaByPathKey, path, prevValue, nextIndexByIdentity);
449
488
  this.reconcileObservableBindings(path, prevValue, nextIndexByIdentity);
@@ -647,7 +686,7 @@ export class FormValueRuntime {
647
686
  this.writeSeq += 1;
648
687
  bumpedWriteSeq = true;
649
688
  }
650
- _.set(this.valuesMirror, p, nextValue);
689
+ _.set(this.valuesMirror, p, this.toMirrorSnapshot(nextValue));
651
690
  hasMirrorChange = true;
652
691
  actuallyChangedPaths.push(p);
653
692
  }
@@ -665,7 +704,11 @@ export class FormValueRuntime {
665
704
 
666
705
  // 非 default 来源写入:需要使默认值永久失效(explicit)
667
706
  for (const p of explicitPathsToMark) {
668
- this.markExplicit(namePathToPathKey(p));
707
+ const pathKey = namePathToPathKey(p);
708
+ this.markExplicit(pathKey);
709
+ if (source === 'user') {
710
+ this.markUserEdited(pathKey);
711
+ }
669
712
  }
670
713
 
671
714
  if (hasMirrorChange) {
@@ -726,6 +769,9 @@ export class FormValueRuntime {
726
769
  seen,
727
770
  );
728
771
  }
772
+ if (prevValue.length !== nextValue.length) {
773
+ return nestedCount + (this.pushExplicitPath(basePath, explicitPaths, seen) ? 1 : 0);
774
+ }
729
775
  return nestedCount;
730
776
  }
731
777
 
@@ -870,7 +916,8 @@ export class FormValueRuntime {
870
916
  } else {
871
917
  form.setFieldsValue?.({ [pathKey]: value });
872
918
  }
873
- _.set(this.valuesMirror, [pathKey], value);
919
+ _.set(this.valuesMirror, [pathKey], this.toMirrorSnapshot(value));
920
+ this.syncMountedFieldModelValue([pathKey], value);
874
921
  }
875
922
  this.bumpChangeTick();
876
923
  } finally {
@@ -885,6 +932,9 @@ export class FormValueRuntime {
885
932
  if (markExplicit) {
886
933
  for (const k of patchKeys) {
887
934
  this.markExplicit(k);
935
+ if (source === 'user') {
936
+ this.markUserEdited(k);
937
+ }
888
938
  }
889
939
  }
890
940
 
@@ -969,7 +1019,8 @@ export class FormValueRuntime {
969
1019
  try {
970
1020
  for (const { namePath, value } of filteredToWrite) {
971
1021
  form.setFieldValue?.(namePath, value);
972
- _.set(this.valuesMirror, namePath, value);
1022
+ _.set(this.valuesMirror, namePath, this.toMirrorSnapshot(value));
1023
+ this.syncMountedFieldModelValue(namePath, value);
973
1024
  changedPaths.push(namePath);
974
1025
  }
975
1026
  this.bumpChangeTick();
@@ -984,13 +1035,16 @@ export class FormValueRuntime {
984
1035
  if (markExplicit) {
985
1036
  for (const { pathKey } of filteredToWrite) {
986
1037
  this.markExplicit(pathKey);
1038
+ if (source === 'user') {
1039
+ this.markUserEdited(pathKey);
1040
+ }
987
1041
  }
988
1042
  }
989
1043
 
990
1044
  if (source === 'default') {
991
1045
  for (const { namePath, pathKey } of filteredToWrite) {
992
1046
  const current = this.getFormValueAtPath(namePath);
993
- this.lastDefaultValueByPathKey.set(pathKey, current);
1047
+ this.lastDefaultValueByPathKey.set(pathKey, this.toMirrorSnapshot(current));
994
1048
  }
995
1049
  }
996
1050
 
@@ -1052,6 +1106,7 @@ export class FormValueRuntime {
1052
1106
  if (this.disposed) return;
1053
1107
  const form = this.getForm?.();
1054
1108
  if (!form) return;
1109
+ if (source === 'override' && this.findUserEditedHit(pathKey)) return;
1055
1110
 
1056
1111
  const prevValue = _.get(this.valuesMirror, namePath);
1057
1112
  if (_.isEqual(prevValue, nextValue)) return;
@@ -1062,7 +1117,8 @@ export class FormValueRuntime {
1062
1117
  this.suppressFormCallbackDepth++;
1063
1118
  try {
1064
1119
  form.setFieldValue?.(namePath, nextValue);
1065
- _.set(this.valuesMirror, namePath, nextValue);
1120
+ _.set(this.valuesMirror, namePath, this.toMirrorSnapshot(nextValue));
1121
+ this.syncMountedFieldModelValue(namePath, nextValue);
1066
1122
  this.bumpChangeTick();
1067
1123
  } finally {
1068
1124
  this.suppressFormCallbackDepth--;
@@ -1098,7 +1154,10 @@ export class FormValueRuntime {
1098
1154
  }
1099
1155
  this.emitFormValuesChange(payload);
1100
1156
 
1101
- if (source === 'default' && this.isExplicit(pathKey)) {
1157
+ if (
1158
+ (source === 'default' && this.isExplicit(pathKey)) ||
1159
+ (source === 'override' && this.findUserEditedHit(pathKey))
1160
+ ) {
1102
1161
  const existing = this.observableBindings.get(pathKey);
1103
1162
  if (existing) {
1104
1163
  existing.dispose();
@@ -1107,7 +1166,7 @@ export class FormValueRuntime {
1107
1166
  }
1108
1167
 
1109
1168
  if (source === 'default') {
1110
- this.lastDefaultValueByPathKey.set(pathKey, this.getFormValueAtPath(namePath));
1169
+ this.lastDefaultValueByPathKey.set(pathKey, this.toMirrorSnapshot(this.getFormValueAtPath(namePath)));
1111
1170
  }
1112
1171
  }
1113
1172
 
@@ -1133,6 +1192,148 @@ export class FormValueRuntime {
1133
1192
  this.model.emitter?.emit?.('formValuesChange', payload);
1134
1193
  }
1135
1194
 
1195
+ private syncMountedFieldModelValue(namePath: NamePath, value: unknown) {
1196
+ const targetKey = namePathToPathKey(namePath);
1197
+ if (!targetKey) return;
1198
+
1199
+ const models = this.mountedFieldModelsByPathKey.get(targetKey);
1200
+ if (!models?.size) return;
1201
+
1202
+ const visited = new Set<FlowModel>();
1203
+ for (const model of Array.from(models)) {
1204
+ this.syncMountedFieldModelValueForModel(model, value, visited);
1205
+ }
1206
+ }
1207
+
1208
+ private syncMountedFieldModelValueForModel(
1209
+ model: FlowModel | null | undefined,
1210
+ value: unknown,
1211
+ visited: Set<FlowModel>,
1212
+ ) {
1213
+ if (!model || visited.has(model)) return;
1214
+ visited.add(model);
1215
+ if (!this.isMountedModelInThisForm(model)) return;
1216
+
1217
+ const nextProps = { value };
1218
+ model.setProps?.(nextProps);
1219
+ const fieldModel = (model as FlowModel & { subModels?: { field?: FlowModel } }).subModels?.field;
1220
+ fieldModel?.setProps?.(nextProps);
1221
+ }
1222
+
1223
+ private rebuildMountedFieldModelIndex() {
1224
+ this.mountedFieldModelsByPathKey.clear();
1225
+ const engine = this.model?.context?.engine;
1226
+ if (!engine || typeof engine.forEachModel !== 'function') return;
1227
+
1228
+ engine.forEachModel((model: FlowModel) => {
1229
+ this.indexMountedFieldModelTree(model);
1230
+ });
1231
+ }
1232
+
1233
+ private indexMountedFieldModelTree(model: FlowModel | null | undefined) {
1234
+ this.visitModelAndForks(model, (item) => this.indexMountedFieldModel(item));
1235
+ }
1236
+
1237
+ private unindexMountedFieldModelTree(model: FlowModel | null | undefined) {
1238
+ this.visitModelAndForks(model, (item) => this.unindexMountedFieldModel(item));
1239
+ }
1240
+
1241
+ private visitModelAndForks(model: FlowModel | null | undefined, visit: (model: FlowModel) => void) {
1242
+ if (!model) return;
1243
+ const visited = new Set<FlowModel>();
1244
+ const walk = (item: FlowModel | null | undefined) => {
1245
+ if (!item || visited.has(item)) return;
1246
+ visited.add(item);
1247
+ visit(item);
1248
+ const forks = (item as FlowModel & { forks?: { forEach?: (cb: (fork: FlowModel) => void) => void } }).forks;
1249
+ forks?.forEach?.((fork) => walk(fork));
1250
+ };
1251
+ walk(model);
1252
+ }
1253
+
1254
+ private indexMountedFieldModel(model: FlowModel) {
1255
+ if (!this.isMountedModelInThisForm(model)) return;
1256
+
1257
+ const pathKeys = new Set<string>();
1258
+ for (const path of this.getMountedModelNamePaths(model)) {
1259
+ const key = namePathToPathKey(path);
1260
+ if (key) pathKeys.add(key);
1261
+ }
1262
+ if (!pathKeys.size) return;
1263
+
1264
+ this.unindexMountedFieldModel(model);
1265
+ this.mountedFieldModelPathKeys.set(model, pathKeys);
1266
+
1267
+ for (const key of pathKeys) {
1268
+ const models = this.mountedFieldModelsByPathKey.get(key) || new Set<FlowModel>();
1269
+ models.add(model);
1270
+ this.mountedFieldModelsByPathKey.set(key, models);
1271
+ }
1272
+ }
1273
+
1274
+ private unindexMountedFieldModel(model: FlowModel) {
1275
+ const pathKeys = this.mountedFieldModelPathKeys.get(model);
1276
+ if (!pathKeys) return;
1277
+
1278
+ for (const key of pathKeys) {
1279
+ const models = this.mountedFieldModelsByPathKey.get(key);
1280
+ if (!models) continue;
1281
+ models.delete(model);
1282
+ if (!models.size) {
1283
+ this.mountedFieldModelsByPathKey.delete(key);
1284
+ }
1285
+ }
1286
+
1287
+ this.mountedFieldModelPathKeys.delete(model);
1288
+ }
1289
+
1290
+ private isMountedModelInThisForm(model: FlowModel) {
1291
+ const blockModel = (model.context as { blockModel?: FlowModel } | undefined)?.blockModel;
1292
+ return !!blockModel && String(blockModel.uid) === String(this.model.uid);
1293
+ }
1294
+
1295
+ private getMountedModelNamePaths(model: FlowModel): NamePath[] {
1296
+ const paths: NamePath[] = [];
1297
+ const push = (value: unknown) => {
1298
+ const path = this.normalizeNamePathValue(value);
1299
+ if (path?.length) paths.push(path);
1300
+ };
1301
+
1302
+ push((model.context as { fieldPathArray?: unknown } | undefined)?.fieldPathArray);
1303
+
1304
+ const props = typeof model.getProps === 'function' ? model.getProps() : (model as { props?: unknown }).props;
1305
+ if (props && typeof props === 'object') {
1306
+ push((props as { name?: unknown }).name);
1307
+ }
1308
+
1309
+ try {
1310
+ const init = model.getStepParams?.('fieldSettings', 'init');
1311
+ if (init && typeof init === 'object') {
1312
+ push((init as { fieldPath?: unknown }).fieldPath);
1313
+ }
1314
+ } catch {
1315
+ // ignore models without field settings
1316
+ }
1317
+
1318
+ const seen = new Set<string>();
1319
+ return paths.filter((path) => {
1320
+ const key = namePathToPathKey(path);
1321
+ if (!key || seen.has(key)) return false;
1322
+ seen.add(key);
1323
+ return true;
1324
+ });
1325
+ }
1326
+
1327
+ private normalizeNamePathValue(value: unknown): NamePath | null {
1328
+ if (Array.isArray(value)) {
1329
+ const path = value.filter((seg): seg is string | number => typeof seg === 'string' || typeof seg === 'number');
1330
+ return path.length === value.length ? path : null;
1331
+ }
1332
+ if (typeof value === 'number') return [value];
1333
+ if (typeof value === 'string' && value) return pathKeyToNamePath(value);
1334
+ return null;
1335
+ }
1336
+
1136
1337
  private markExplicit(pathKey: string) {
1137
1338
  if (this.explicitSet.has(pathKey)) return;
1138
1339
  this.explicitSet.add(pathKey);
@@ -1154,6 +1355,18 @@ export class FormValueRuntime {
1154
1355
  }
1155
1356
  }
1156
1357
 
1358
+ private markUserEdited(pathKey: string) {
1359
+ if (this.userEditedSet.has(pathKey)) return;
1360
+ this.userEditedSet.add(pathKey);
1361
+
1362
+ for (const [k, binding] of Array.from(this.observableBindings.entries())) {
1363
+ if (binding.source !== 'override') continue;
1364
+ if (!this.findUserEditedHit(k)) continue;
1365
+ binding.dispose();
1366
+ this.observableBindings.delete(k);
1367
+ }
1368
+ }
1369
+
1157
1370
  private normalizeObservedNamePath(name: NamePath | string | number | undefined): NamePath | null {
1158
1371
  if (Array.isArray(name)) return name as NamePath;
1159
1372
  if (typeof name === 'number') return [name];
@@ -1216,4 +1429,35 @@ export class FormValueRuntime {
1216
1429
  }
1217
1430
  return null;
1218
1431
  }
1432
+
1433
+ private findUserEditedHit(pathKey: string): string | null {
1434
+ if (this.userEditedSet.has(pathKey)) return pathKey;
1435
+ const namePath = pathKeyToNamePath(pathKey);
1436
+ const prefix: NamePath = [];
1437
+
1438
+ for (let i = 0; i < namePath.length; i++) {
1439
+ prefix.push(namePath[i]);
1440
+ const key = namePathToPathKey(prefix as any);
1441
+ if (!this.userEditedSet.has(key)) continue;
1442
+
1443
+ const nextSeg = namePath[i + 1];
1444
+ if (typeof nextSeg === 'number') {
1445
+ continue;
1446
+ }
1447
+ return key;
1448
+ }
1449
+ for (const key of this.userEditedSet) {
1450
+ if (!this.isDescendantPathKey(key, pathKey)) continue;
1451
+ return key;
1452
+ }
1453
+ return null;
1454
+ }
1455
+
1456
+ private isDescendantPathKey(candidateKey: string, parentKey: string) {
1457
+ if (!candidateKey || !parentKey || candidateKey === parentKey) return false;
1458
+ const candidatePath = pathKeyToNamePath(candidateKey);
1459
+ const parentPath = pathKeyToNamePath(parentKey);
1460
+ if (candidatePath.length <= parentPath.length) return false;
1461
+ return parentPath.every((seg, index) => candidatePath[index] === seg);
1462
+ }
1219
1463
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  export type NamePath = Array<string | number>;
11
11
 
12
- export type ValueSource = 'default' | 'linkage' | 'user' | 'system';
12
+ export type ValueSource = 'default' | 'linkage' | 'user' | 'system' | 'override';
13
13
 
14
14
  export type Patch =
15
15
  | Record<string, any>
@@ -57,7 +57,7 @@ export interface FormValuesChangePayload {
57
57
  allValuesSnapshot?: any;
58
58
  }
59
59
 
60
- export type AssignMode = 'default' | 'assign';
60
+ export type AssignMode = 'default' | 'assign' | 'override';
61
61
 
62
62
  export type FormAssignRuleItem = {
63
63
  key?: string;
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { ElementProxy, tExpr, createSafeDocument, createSafeWindow, createSafeNavigator } from '@nocobase/flow-engine';
10
+ import { ElementProxy, tExpr } from '@nocobase/flow-engine';
11
11
  import React from 'react';
12
12
  import { BlockModel } from '../../base';
13
13
  import { BlockItemCard } from '../../../components';
@@ -167,12 +167,7 @@ ctx.render(\`
167
167
  },
168
168
  },
169
169
  });
170
- const navigator = createSafeNavigator();
171
- await ctx.runjs(
172
- code,
173
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
174
- { version },
175
- );
170
+ await ctx.runjs(code, undefined, { version });
176
171
  });
177
172
  },
178
173
  },
@@ -19,9 +19,6 @@ import {
19
19
  createRecordMetaFactory,
20
20
  createRecordResolveOnServerWithLocal,
21
21
  ElementProxy,
22
- createSafeDocument,
23
- createSafeWindow,
24
- createSafeNavigator,
25
22
  observer,
26
23
  } from '@nocobase/flow-engine';
27
24
  import { Tooltip } from 'antd';
@@ -294,12 +291,7 @@ JSColumnModel.registerFlow({
294
291
  get: () => new ElementProxy((ctx.ref?.current as HTMLElement | null) || element),
295
292
  cache: false,
296
293
  });
297
- const navigator = createSafeNavigator();
298
- await ctx.runjs(
299
- code,
300
- { window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
301
- { version },
302
- );
294
+ await ctx.runjs(code, undefined, { version });
303
295
  });
304
296
  },
305
297
  },
@@ -140,7 +140,7 @@ const AddActionToolbarComponent = observer(({ model }: any) => {
140
140
  subModelBaseClass={model.context.getModelClassName('RecordActionGroupModel')}
141
141
  subModelKey="actions"
142
142
  afterSubModelInit={async (actionModel) => {
143
- actionModel.setStepParams('buttonSettings', 'general', { type: 'link', icon: null });
143
+ actionModel.setStepParams('buttonSettings', 'general', { type: 'link', icon: null, iconOnly: false });
144
144
  }}
145
145
  >
146
146
  <PlusOutlined />
@@ -179,11 +179,21 @@ const AddFieldColumn = ({ model }: { model: TableBlockModel }) => {
179
179
  ].filter(Boolean)}
180
180
  keepDropdownOpen
181
181
  >
182
- <FlowSettingsButton icon={<SettingOutlined />}>{model.translate('Fields')}</FlowSettingsButton>
182
+ {renderTableSettingsButton(model, 'Fields')}
183
183
  </AddSubModelButton>
184
184
  );
185
185
  };
186
186
 
187
+ function renderTableSettingsButton(model: TableBlockModel, title: string) {
188
+ const label = model.translate(title);
189
+
190
+ return (
191
+ <FlowSettingsButton aria-label={label} icon={<SettingOutlined />}>
192
+ {model.context.isMobileLayout ? null : label}
193
+ </FlowSettingsButton>
194
+ );
195
+ }
196
+
187
197
  type CustomTableBlockModelClassesEnum = {
188
198
  CollectionActionGroupModel?: string;
189
199
  RecordActionGroupModel?: string;
@@ -265,7 +275,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
265
275
  cols.push({
266
276
  key: 'addColumn',
267
277
  fixed: 'right',
268
- width: 100,
278
+ ...(this.context.isMobileLayout ? {} : { width: 100 }),
269
279
  title: <AddFieldColumn model={this} />,
270
280
  } as any);
271
281
  }
@@ -508,7 +518,7 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
508
518
  subModelBaseClass={this.getModelClassName('CollectionActionGroupModel')}
509
519
  subModelKey="actions"
510
520
  >
511
- <FlowSettingsButton icon={<SettingOutlined />}>{this.translate('Actions')}</FlowSettingsButton>
521
+ {renderTableSettingsButton(this, 'Actions')}
512
522
  </AddSubModelButton>
513
523
  );
514
524
  }