@nocobase/client-v2 2.1.0-beta.26 → 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 (83) 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/components/code-editor/types.d.ts +1 -0
  6. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  7. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  8. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
  9. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  10. package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
  11. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
  12. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  13. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  14. package/es/flow-compat/data.d.ts +9 -2
  15. package/es/flow-compat/index.d.ts +1 -1
  16. package/es/index.d.ts +1 -1
  17. package/es/index.mjs +97 -90
  18. package/lib/index.js +99 -92
  19. package/package.json +6 -5
  20. package/src/BaseApplication.tsx +1 -1
  21. package/src/__tests__/app.test.tsx +23 -6
  22. package/src/components/form/JsonTextArea.tsx +129 -0
  23. package/src/components/index.ts +1 -0
  24. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  25. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  26. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  27. package/src/flow/actions/index.ts +1 -0
  28. package/src/flow/actions/linkageRules.tsx +117 -19
  29. package/src/flow/actions/openView.tsx +2 -1
  30. package/src/flow/actions/pattern.tsx +25 -2
  31. package/src/flow/actions/titleField.tsx +8 -3
  32. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  33. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  34. package/src/flow/components/FieldAssignValueInput.tsx +1 -0
  35. package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
  36. package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
  37. package/src/flow/components/code-editor/index.tsx +18 -17
  38. package/src/flow/components/code-editor/linter.ts +222 -158
  39. package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
  40. package/src/flow/components/code-editor/types.ts +1 -0
  41. package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
  42. package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
  43. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
  44. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
  45. package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
  46. package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
  47. package/src/flow/models/actions/FilterActionModel.tsx +17 -9
  48. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  49. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  50. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  51. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  52. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
  53. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
  54. package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
  55. package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
  56. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
  57. package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
  58. package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
  59. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
  60. package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
  61. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  62. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  63. package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
  64. package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
  65. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
  66. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  67. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  68. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  69. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
  70. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  71. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  72. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  73. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  74. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  75. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  76. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  77. package/src/flow/models/fields/DividerItemModel.tsx +30 -15
  78. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  79. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  80. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
  81. package/src/flow-compat/data.ts +25 -3
  82. package/src/flow-compat/index.ts +7 -1
  83. package/src/index.ts +1 -1
@@ -22,6 +22,7 @@ import { FieldModel } from '../../base/FieldModel';
22
22
  import { DetailsItemModel } from '../details/DetailsItemModel';
23
23
  import { EditFormModel } from './EditFormModel';
24
24
  import _ from 'lodash';
25
+ import { Tooltip } from 'antd';
25
26
  import { coerceForToOneField } from '../../../internal/utils/associationValueCoercion';
26
27
  import { buildDynamicNamePath } from './dynamicNamePath';
27
28
 
@@ -99,54 +100,63 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
99
100
 
100
101
  renderItem() {
101
102
  const fieldModel = this.subModels.field as FieldModel;
102
- // 行索引(来自数组子表单)
103
103
  const idx = this.context.fieldIndex;
104
104
  const fieldKey = this.context.fieldKey;
105
105
  const parentFieldPathArray = this.parent?.context.fieldPathArray || [];
106
+ const isHiddenReservedValuePreview =
107
+ !!this.context.flowSettingsEnabled && !!this.props.hidden && !this.hidden && !this.forbidden;
106
108
 
107
- // 嵌套场景下继续传透,为字段子模型创建 fork
108
109
  const modelForRender =
109
- idx != null
110
+ idx != null || isHiddenReservedValuePreview
110
111
  ? (() => {
111
- const fork = fieldModel.createFork({}, `${fieldKey}`);
112
- fork.context.defineProperty('fieldIndex', {
113
- get: () => idx,
114
- });
115
- fork.context.defineProperty('fieldKey', {
116
- get: () => fieldKey,
117
- });
118
- if (this.context.currentObject) {
119
- fork.context.defineProperty('currentObject', {
120
- get: () => this.context.currentObject,
112
+ const forkKey = isHiddenReservedValuePreview ? `${this.uid}:config-visible` : `${fieldKey}`;
113
+ const fork = fieldModel.createFork({}, forkKey);
114
+ if (idx != null) {
115
+ fork.context.defineProperty('fieldIndex', {
116
+ get: () => idx,
121
117
  });
122
- }
123
- const itemOptions = this.context.getPropertyOptions('item');
124
- if (this.context.item) {
125
- const { value: _value, ...rest } = (itemOptions || {}) as any;
126
- fork.context.defineProperty('item', {
127
- ...rest,
128
- get: () => this.context.item,
129
- cache: false,
118
+ fork.context.defineProperty('fieldKey', {
119
+ get: () => fieldKey,
130
120
  });
121
+ if (this.context.currentObject) {
122
+ fork.context.defineProperty('currentObject', {
123
+ get: () => this.context.currentObject,
124
+ });
125
+ }
126
+ const itemOptions = this.context.getPropertyOptions('item');
127
+ if (this.context.item) {
128
+ const { value: _value, ...rest } = (itemOptions || {}) as any;
129
+ fork.context.defineProperty('item', {
130
+ ...rest,
131
+ get: () => this.context.item,
132
+ cache: false,
133
+ });
134
+ }
135
+ if (this.context.pattern) {
136
+ fork.context.defineProperty('pattern', {
137
+ get: () => this.context.pattern,
138
+ });
139
+ }
131
140
  }
132
- if (this.context.pattern) {
133
- fork.context.defineProperty('pattern', {
134
- get: () => this.context.pattern,
135
- });
141
+ if (isHiddenReservedValuePreview) {
142
+ fork.setProps({ hidden: false });
136
143
  }
137
144
  return fork;
138
145
  })()
139
146
  : fieldModel;
140
147
  const mergedProps = this.context.pattern ? { ...this.props, pattern: this.context.pattern } : this.props;
141
- const { initialValue, ...mergedPropsWithoutInitial } = mergedProps as any;
148
+ const { initialValue, hidden, ...mergedPropsWithoutInitial } = mergedProps as any;
149
+ const formItemProps = isHiddenReservedValuePreview
150
+ ? mergedPropsWithoutInitial
151
+ : { hidden, ...mergedPropsWithoutInitial };
142
152
  const fieldPath = buildDynamicNamePath(this.props.name, idx);
143
153
  this.context.defineProperty('fieldPathArray', {
144
154
  value: [...parentFieldPathArray, ..._.castArray(fieldPath)],
145
155
  });
146
156
  const record = this.context.item?.value || this.context.record;
147
- return (
157
+ const content = (
148
158
  <FormItem
149
- {...mergedPropsWithoutInitial}
159
+ {...formItemProps}
150
160
  name={fieldPath}
151
161
  validateFirst={true}
152
162
  disabled={
@@ -158,6 +168,16 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
158
168
  <FieldModelRenderer model={modelForRender} name={fieldPath} />
159
169
  </FormItem>
160
170
  );
171
+
172
+ if (isHiddenReservedValuePreview) {
173
+ return (
174
+ <Tooltip title={this.context.t('The field is hidden and only visible when the UI Editor is active')}>
175
+ <div style={{ opacity: 0.3 }}>{content}</div>
176
+ </Tooltip>
177
+ );
178
+ }
179
+
180
+ return content;
161
181
  }
162
182
  }
163
183
 
@@ -159,12 +159,141 @@ describe('FormValueRuntime (default rules)', () => {
159
159
  await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'Y' }], { source: 'user' });
160
160
  await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Y'));
161
161
 
162
+ // change dependency again -> default keeps following while target is still the last default
163
+ await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'Z' }], { source: 'user' });
164
+ await waitFor(() => expect(formStub.getFieldValue(['a'])).toBe('Z'));
165
+
162
166
  // user changes target -> default should be disabled permanently
163
167
  await runtime.setFormValues(fieldCtx, [{ path: ['a'], value: 'user' }], { source: 'user' });
164
- await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'Z' }], { source: 'user' });
168
+ await runtime.setFormValues(fieldCtx, [{ path: ['b'], value: 'W' }], { source: 'user' });
165
169
  expect(formStub.getFieldValue(['a'])).toBe('user');
166
170
  });
167
171
 
172
+ it('allows direct default patches to keep following until target is explicitly changed', async () => {
173
+ const engineEmitter = new EventEmitter();
174
+ const blockEmitter = new EventEmitter();
175
+ const formStub = createFormStub({ roleUid: 'role-uid-1', roleName: '' });
176
+
177
+ const blockModel: any = {
178
+ uid: 'form-direct-default-patch',
179
+ flowEngine: { emitter: engineEmitter },
180
+ emitter: blockEmitter,
181
+ dispatchEvent: vi.fn(),
182
+ getAclActionName: () => 'create',
183
+ };
184
+
185
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
186
+ runtime.mount({ sync: true });
187
+
188
+ const blockCtx = createFieldContext(runtime);
189
+ blockModel.context = blockCtx;
190
+
191
+ expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-1')).toBe(true);
192
+ await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-1' }], {
193
+ source: 'linkage',
194
+ txId: 'tx-linkage-1',
195
+ linkageTxId: 'tx-linkage-1',
196
+ });
197
+ runtime.recordDefaultValuePatch(['roleName'], 'role-uid-1');
198
+
199
+ await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-2' }], { source: 'user' });
200
+ expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-2')).toBe(true);
201
+ await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-2' }], {
202
+ source: 'linkage',
203
+ txId: 'tx-linkage-2',
204
+ linkageTxId: 'tx-linkage-2',
205
+ });
206
+ runtime.recordDefaultValuePatch(['roleName'], 'role-uid-2');
207
+
208
+ await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-3' }], { source: 'user' });
209
+ expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-3')).toBe(true);
210
+
211
+ await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'manual' }], { source: 'user' });
212
+ expect(runtime.canApplyDefaultValuePatch(['roleName'], 'role-uid-4')).toBe(false);
213
+ });
214
+
215
+ it('keeps direct default patches active when an unchanged default value is carried by a row change', async () => {
216
+ const engineEmitter = new EventEmitter();
217
+ const blockEmitter = new EventEmitter();
218
+ const formStub = createFormStub({ roles: [{ uid: '1', title: '' }] });
219
+
220
+ const blockModel: any = {
221
+ uid: 'form-direct-default-patch-row',
222
+ flowEngine: { emitter: engineEmitter },
223
+ emitter: blockEmitter,
224
+ dispatchEvent: vi.fn(),
225
+ getAclActionName: () => 'create',
226
+ };
227
+
228
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
229
+ runtime.mount({ sync: true });
230
+
231
+ const blockCtx = createFieldContext(runtime);
232
+ blockModel.context = blockCtx;
233
+
234
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '1')).toBe(true);
235
+ await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: '1' }], {
236
+ source: 'linkage',
237
+ txId: 'tx-linkage-row-1',
238
+ linkageTxId: 'tx-linkage-row-1',
239
+ });
240
+ runtime.recordDefaultValuePatch(['roles', 0, 'title'], '1');
241
+
242
+ lodashSet((runtime as any).valuesMirror, ['roles', 0, 'title'], undefined);
243
+ runtime.handleFormFieldsChange([{ name: ['roles', 0, 'title'], touched: true } as any]);
244
+ expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
245
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
246
+
247
+ (runtime as any).markExplicit('roles');
248
+ expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
249
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
250
+
251
+ lodashSet((runtime as any).valuesMirror, ['roles', 0, 'title'], undefined);
252
+
253
+ lodashSet((formStub as any).__store, ['roles', 0, 'uid'], '12');
254
+ runtime.handleFormValuesChange({ roles: formStub.getFieldValue(['roles']) }, formStub.getFieldsValue());
255
+
256
+ expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
257
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
258
+ await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: '12' }], {
259
+ source: 'linkage',
260
+ txId: 'tx-linkage-row-2',
261
+ linkageTxId: 'tx-linkage-row-2',
262
+ });
263
+ runtime.recordDefaultValuePatch(['roles', 0, 'title'], '12');
264
+ expect(formStub.getFieldValue(['roles', 0, 'title'])).toBe('12');
265
+
266
+ await runtime.setFormValues(blockCtx, [{ path: ['roles', 0, 'title'], value: 'manual' }], { source: 'user' });
267
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '13')).toBe(false);
268
+ });
269
+
270
+ it('does not mark omitted sibling fields from partial top-level changedValues as explicit', async () => {
271
+ const engineEmitter = new EventEmitter();
272
+ const blockEmitter = new EventEmitter();
273
+ const formStub = createFormStub({
274
+ roles: [{ __is_new__: true, __index__: 'row-1', name: '1', title: '1' }],
275
+ });
276
+
277
+ const blockModel: any = {
278
+ uid: 'form-direct-default-patch-partial-row',
279
+ flowEngine: { emitter: engineEmitter },
280
+ emitter: blockEmitter,
281
+ dispatchEvent: vi.fn(),
282
+ getAclActionName: () => 'create',
283
+ };
284
+
285
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
286
+ runtime.mount({ sync: true });
287
+
288
+ runtime.recordDefaultValuePatch(['roles', 0, 'title'], '1');
289
+
290
+ lodashSet((formStub as any).__store, ['roles', 0, 'name'], '12');
291
+ runtime.handleFormValuesChange({ roles: [{ name: '12' }] }, formStub.getFieldsValue());
292
+
293
+ expect((runtime as any).findExplicitHit('roles[0].title')).toBeNull();
294
+ expect(runtime.canApplyDefaultValuePatch(['roles', 0, 'title'], '12')).toBe(true);
295
+ });
296
+
168
297
  it('handles onFieldsChange name as string and triggers default recompute', async () => {
169
298
  const engineEmitter = new EventEmitter();
170
299
  const blockEmitter = new EventEmitter();
@@ -3267,4 +3396,41 @@ describe('FormValueRuntime (form assign rules)', () => {
3267
3396
  expect(payload.txId).toBe('tx-current');
3268
3397
  expect(payload.linkageTxId).toBe('tx-root');
3269
3398
  });
3399
+
3400
+ it('does not mark linkage writes explicit so later user edits can keep following default linkage', async () => {
3401
+ const engineEmitter = new EventEmitter();
3402
+ const blockEmitter = new EventEmitter();
3403
+ const formStub = createFormStub({ roleUid: 'role-uid-1', roleName: '' });
3404
+
3405
+ const blockModel: any = {
3406
+ uid: 'form-linkage-linkage-not-explicit',
3407
+ flowEngine: { emitter: engineEmitter },
3408
+ emitter: blockEmitter,
3409
+ dispatchEvent: vi.fn(),
3410
+ getAclActionName: () => 'create',
3411
+ };
3412
+
3413
+ const runtime = new FormValueRuntime({ model: blockModel, getForm: () => formStub as any });
3414
+ runtime.mount({ sync: true });
3415
+
3416
+ const blockCtx = createFieldContext(runtime);
3417
+ blockModel.context = blockCtx;
3418
+
3419
+ await runtime.setFormValues(blockCtx, [{ path: ['roleName'], value: 'role-uid-1' }], {
3420
+ source: 'linkage',
3421
+ txId: 'tx-linkage-1',
3422
+ linkageTxId: 'tx-linkage-1',
3423
+ linkageScopeDepth: 0,
3424
+ });
3425
+
3426
+ expect(formStub.getFieldValue(['roleName'])).toBe('role-uid-1');
3427
+ expect((runtime as any).findExplicitHit('roleName')).toBeNull();
3428
+
3429
+ await runtime.setFormValues(blockCtx, [{ path: ['roleUid'], value: 'role-uid-2' }], {
3430
+ source: 'user',
3431
+ txId: 'tx-user-1',
3432
+ });
3433
+ expect(formStub.getFieldValue(['roleUid'])).toBe('role-uid-2');
3434
+ expect((runtime as any).findExplicitHit('roleName')).toBeNull();
3435
+ });
3270
3436
  });
@@ -137,6 +137,42 @@ export class FormValueRuntime {
137
137
  return this.getForm().getFieldsValue(true);
138
138
  }
139
139
 
140
+ canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
141
+ if (!namePath?.length) return false;
142
+ if (typeof resolved === 'undefined') return false;
143
+
144
+ const pathKey = namePathToPathKey(namePath);
145
+ const current = this.getFormValueAtPath(namePath);
146
+ const last = this.lastDefaultValueByPathKey.get(pathKey);
147
+ const nextSnapshot = isObservable(resolved) ? toJS(resolved) : resolved;
148
+ const currentSnapshot = isObservable(current) ? toJS(current) : current;
149
+ const currentEqualsLastDefault = typeof last !== 'undefined' && _.isEqual(currentSnapshot, last);
150
+ const explicitHit = this.findExplicitHit(pathKey);
151
+
152
+ if (explicitHit && !currentEqualsLastDefault) return false;
153
+
154
+ const canOverwrite = isEmptyValue(current) || currentEqualsLastDefault;
155
+ if (!canOverwrite && _.isEqual(current, nextSnapshot)) {
156
+ this.lastDefaultValueByPathKey.set(pathKey, nextSnapshot);
157
+ return false;
158
+ }
159
+
160
+ return canOverwrite;
161
+ }
162
+
163
+ recordDefaultValuePatch(namePath: NamePath, value?: any) {
164
+ if (!namePath?.length) return;
165
+ const pathKey = namePathToPathKey(namePath);
166
+ const snapshot =
167
+ arguments.length >= 2 ? (isObservable(value) ? toJS(value) : value) : this.getFormValueAtPath(namePath);
168
+ this.lastDefaultValueByPathKey.set(pathKey, snapshot);
169
+ const current = this.getFormValueAtPath(namePath);
170
+ const currentSnapshot = isObservable(current) ? toJS(current) : current;
171
+ if (_.isEqual(currentSnapshot, snapshot)) {
172
+ this.clearExplicitForDefaultPatch(pathKey);
173
+ }
174
+ }
175
+
140
176
  private getFormValueAtPath(namePath: NamePath) {
141
177
  const form: any = this.getForm?.();
142
178
  if (form && typeof form.getFieldValue === 'function') {
@@ -247,11 +283,7 @@ export class FormValueRuntime {
247
283
  let bumpedWriteSeq = false;
248
284
  for (const field of changedFields || []) {
249
285
  const name = field?.name;
250
- const namePath: NamePath | null = Array.isArray(name)
251
- ? (name as NamePath)
252
- : typeof name === 'string' || typeof name === 'number'
253
- ? ([name] as NamePath)
254
- : null;
286
+ const namePath = this.normalizeObservedNamePath(name);
255
287
  if (!namePath?.length) continue;
256
288
  const nextValue = form.getFieldValue(namePath as any);
257
289
  const prevValue = _.get(this.valuesMirror, namePath);
@@ -265,8 +297,11 @@ export class FormValueRuntime {
265
297
  const isMeaningfulTouched =
266
298
  field?.touched === true && !this.shouldIgnoreSyntheticTouchedInit(namePath, prevValue, nextValue);
267
299
  if (isMeaningfulTouched) {
268
- touchedChangedPathKeys.add(namePathToPathKey(namePath));
269
- hasMeaningfulTouchedChange = true;
300
+ const pathKey = namePathToPathKey(namePath);
301
+ if (!this.shouldKeepDefaultPathValueEnabled(namePath, nextValue)) {
302
+ touchedChangedPathKeys.add(pathKey);
303
+ hasMeaningfulTouchedChange = true;
304
+ }
270
305
  }
271
306
  }
272
307
 
@@ -526,6 +561,10 @@ export class FormValueRuntime {
526
561
  }
527
562
 
528
563
  private getObservedChangedValue(path: NamePath, changedValues: any, snapshot: any) {
564
+ if (_.has(snapshot, path as any)) {
565
+ return _.get(snapshot, path as any);
566
+ }
567
+
529
568
  if (path?.length === 1) {
530
569
  const key = path[0];
531
570
  if (
@@ -546,6 +585,7 @@ export class FormValueRuntime {
546
585
 
547
586
  let observed = snapshot;
548
587
  for (const key of Object.keys(changedValues)) {
588
+ if (_.has(observed, [key])) continue;
549
589
  if (observed === snapshot) {
550
590
  observed = Array.isArray(snapshot) ? [...snapshot] : { ...snapshot };
551
591
  }
@@ -573,7 +613,15 @@ export class FormValueRuntime {
573
613
  this.lastObservedChangedPaths = null;
574
614
  this.lastObservedSource = null;
575
615
 
576
- const rawSnapshot = allValues && typeof allValues === 'object' ? allValues : this.getFormValuesSnapshot();
616
+ // 子表格的 changedValues 可能只包含局部行对象,diff 必须以 form 当前完整快照为准,
617
+ // 否则缺失的 sibling 字段会被误判为用户清空。
618
+ const formSnapshot = this.getFormValuesSnapshot();
619
+ const rawSnapshot =
620
+ formSnapshot && typeof formSnapshot === 'object'
621
+ ? formSnapshot
622
+ : allValues && typeof allValues === 'object'
623
+ ? allValues
624
+ : changedValues;
577
625
  const snapshot = this.getObservedSnapshot(changedValues, rawSnapshot);
578
626
  this.reconcileArrayItemState([...rawChangedPaths, ...changedValuePaths], changedValues, snapshot);
579
627
  this.pruneDeletedArrayItemState(snapshot);
@@ -602,8 +650,16 @@ export class FormValueRuntime {
602
650
  this.bumpChangeTick();
603
651
  }
604
652
 
605
- // default 来源写入:需要使默认值永久失效(explicit)
653
+ const explicitPathsToMark: NamePath[] = [];
606
654
  for (const p of explicitPaths) {
655
+ if (this.shouldKeepDefaultPathEnabled(p, snapshot)) {
656
+ continue;
657
+ }
658
+ explicitPathsToMark.push(p);
659
+ }
660
+
661
+ // 非 default 来源写入:需要使默认值永久失效(explicit)
662
+ for (const p of explicitPathsToMark) {
607
663
  this.markExplicit(namePathToPathKey(p));
608
664
  }
609
665
 
@@ -733,7 +789,7 @@ export class FormValueRuntime {
733
789
  const source: ValueSource = options?.source ?? 'system';
734
790
  const triggerEvent = options?.triggerEvent !== false;
735
791
  const txId = options?.txId ?? createTxId();
736
- const markExplicit = options?.markExplicit ?? source !== 'default';
792
+ const markExplicit = options?.markExplicit ?? (source !== 'default' && source !== 'linkage');
737
793
  const ownsTxId = typeof options?.txId === 'undefined';
738
794
 
739
795
  const linkageScopeDepth =
@@ -1071,10 +1127,12 @@ export class FormValueRuntime {
1071
1127
  private markExplicit(pathKey: string) {
1072
1128
  if (this.explicitSet.has(pathKey)) return;
1073
1129
  this.explicitSet.add(pathKey);
1130
+ const preserveDescendantDefaults = this.shouldPreserveDescendantDefaultsOnExplicit(pathKey);
1074
1131
 
1075
1132
  // explicit 后默认值永远失效:清理该路径及其子路径的 lastDefault 记录,避免误判“仍是默认值”
1076
1133
  for (const k of Array.from(this.lastDefaultValueByPathKey.keys())) {
1077
- if (k === pathKey || k.startsWith(`${pathKey}.`) || k.startsWith(`${pathKey}[`)) {
1134
+ const isDescendant = k.startsWith(`${pathKey}.`) || k.startsWith(`${pathKey}[`);
1135
+ if (k === pathKey || (!preserveDescendantDefaults && isDescendant)) {
1078
1136
  this.lastDefaultValueByPathKey.delete(k);
1079
1137
  }
1080
1138
  }
@@ -1087,6 +1145,40 @@ export class FormValueRuntime {
1087
1145
  }
1088
1146
  }
1089
1147
 
1148
+ private normalizeObservedNamePath(name: NamePath | string | number | undefined): NamePath | null {
1149
+ if (Array.isArray(name)) return name as NamePath;
1150
+ if (typeof name === 'number') return [name];
1151
+ if (typeof name !== 'string' || !name) return null;
1152
+ const parsed = pathKeyToNamePath(name);
1153
+ return parsed.length ? parsed : [name];
1154
+ }
1155
+
1156
+ private clearExplicitForDefaultPatch(pathKey: string) {
1157
+ for (const key of Array.from(this.explicitSet)) {
1158
+ if (key === pathKey || key.startsWith(`${pathKey}.`) || key.startsWith(`${pathKey}[`)) {
1159
+ this.explicitSet.delete(key);
1160
+ }
1161
+ }
1162
+ }
1163
+
1164
+ private shouldPreserveDescendantDefaultsOnExplicit(pathKey: string) {
1165
+ const value = this.getFormValueAtPath(pathKeyToNamePath(pathKey));
1166
+ return Array.isArray(value);
1167
+ }
1168
+
1169
+ private shouldKeepDefaultPathEnabled(namePath: NamePath, snapshot: any) {
1170
+ return this.shouldKeepDefaultPathValueEnabled(namePath, _.get(snapshot, namePath as any));
1171
+ }
1172
+
1173
+ private shouldKeepDefaultPathValueEnabled(namePath: NamePath, value: any) {
1174
+ const pathKey = namePathToPathKey(namePath);
1175
+ const last = this.lastDefaultValueByPathKey.get(pathKey);
1176
+ if (typeof last === 'undefined') return false;
1177
+
1178
+ const nextSnapshot = isObservable(value) ? toJS(value) : value;
1179
+ return _.isEqual(nextSnapshot, last);
1180
+ }
1181
+
1090
1182
  private isExplicit(pathKey: string) {
1091
1183
  return !!this.findExplicitHit(pathKey);
1092
1184
  }
@@ -0,0 +1,14 @@
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
+ export function isArrayLikeField(field: any) {
11
+ return (
12
+ ['multipleSelect', 'checkboxGroup'].includes(field?.interface) || ['array', 'json', 'jsonb'].includes(field?.type)
13
+ );
14
+ }
@@ -563,6 +563,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
563
563
  columns={this.columns.value}
564
564
  pagination={this.pagination()}
565
565
  highlightedRowKey={highlightedRowKey}
566
+ selectedRowKeysFromResource={this.resource
567
+ .getSelectedRows()
568
+ .map((row) => getRowKey(row, this.collection.filterTargetKey))}
566
569
  defaultExpandAllRows={this.props.defaultExpandAllRows}
567
570
  expandedRowKeys={this.props.expandedRowKeys}
568
571
  heightMode={heightMode}
@@ -581,6 +584,7 @@ const TableBlockContent = (props: {
581
584
  columns: any;
582
585
  pagination: any;
583
586
  highlightedRowKey: string;
587
+ selectedRowKeysFromResource: string[];
584
588
  defaultExpandAllRows?: boolean;
585
589
  expandedRowKeys?: any[];
586
590
  heightMode?: string;
@@ -594,6 +598,7 @@ const TableBlockContent = (props: {
594
598
  columns,
595
599
  pagination,
596
600
  highlightedRowKey,
601
+ selectedRowKeysFromResource,
597
602
  defaultExpandAllRows,
598
603
  expandedRowKeys,
599
604
  heightMode,
@@ -620,6 +625,7 @@ const TableBlockContent = (props: {
620
625
  columns={columns}
621
626
  pagination={pagination}
622
627
  highlightedRowKey={highlightedRowKey}
628
+ selectedRowKeysFromResource={selectedRowKeysFromResource}
623
629
  defaultExpandAllRows={defaultExpandAllRows}
624
630
  expandedRowKeys={expandedRowKeys}
625
631
  tableScroll={tableScroll}
@@ -829,6 +835,7 @@ const HighPerformanceTable = React.memo(
829
835
  columns: any;
830
836
  pagination: any;
831
837
  highlightedRowKey: string;
838
+ selectedRowKeysFromResource: string[];
832
839
  defaultExpandAllRows?: boolean;
833
840
  expandedRowKeys?: any[];
834
841
  tableScroll;
@@ -841,6 +848,7 @@ const HighPerformanceTable = React.memo(
841
848
  columns,
842
849
  pagination: _pagination,
843
850
  highlightedRowKey,
851
+ selectedRowKeysFromResource,
844
852
  defaultExpandAllRows,
845
853
  expandedRowKeys,
846
854
  tableScroll,
@@ -848,9 +856,17 @@ const HighPerformanceTable = React.memo(
848
856
  const dataSourceRef = useRef(dataSource);
849
857
  dataSourceRef.current = dataSource;
850
858
 
851
- const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(() =>
852
- model.resource.getSelectedRows().map((row) => getRowKey(row, model.collection.filterTargetKey)),
853
- );
859
+ const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(selectedRowKeysFromResource || []);
860
+
861
+ useEffect(() => {
862
+ const nextSelectedRowKeys = selectedRowKeysFromResource || [];
863
+ setSelectedRowKeys((prev) => {
864
+ if (_.isEqual(prev, nextSelectedRowKeys)) {
865
+ return prev;
866
+ }
867
+ return nextSelectedRowKeys;
868
+ });
869
+ }, [selectedRowKeysFromResource]);
854
870
 
855
871
  const getRowKeyFunc = useCallback(
856
872
  (record) => {
@@ -177,6 +177,25 @@ const MemoFieldRenderer = React.memo(FieldModelRenderer, (prev, next) => {
177
177
  return prev.value === next.value && prev.model === next.model;
178
178
  });
179
179
 
180
+ export function buildRowPathFromFieldIndex(fieldIndex: unknown): Array<string | number> | null {
181
+ if (!Array.isArray(fieldIndex) || !fieldIndex.length) return null;
182
+ const out: Array<string | number> = [];
183
+ for (const entry of fieldIndex) {
184
+ if (typeof entry !== 'string') continue;
185
+ const [fieldName, indexStr] = entry.split(':');
186
+ const index = Number(indexStr);
187
+ if (!fieldName || !Number.isFinite(index)) continue;
188
+ out.push(fieldName, index);
189
+ }
190
+ return out.length ? out : null;
191
+ }
192
+
193
+ export function getLatestSubTableRowRecord(form: any, fieldIndex: unknown, fallbackRecord: any): any {
194
+ const latestRowPath = buildRowPathFromFieldIndex(fieldIndex);
195
+ const latestRecord = latestRowPath ? form?.getFieldValue?.(latestRowPath) : undefined;
196
+ return typeof latestRecord === 'undefined' ? fallbackRecord : latestRecord;
197
+ }
198
+
180
199
  function shouldCommitImmediately(value: any) {
181
200
  if (Array.isArray(value)) {
182
201
  return true;
@@ -574,6 +593,9 @@ export class SubTableColumnModel<
574
593
  const rowForkKey = `row:${baseIndexKey}:${rowIdentity}:${String(rowIdx)}`;
575
594
  const rowFork: any = (() => {
576
595
  const fork = this.createFork({}, rowForkKey);
596
+ fork.context.defineProperty('subTableRowFork', {
597
+ value: true,
598
+ });
577
599
  const associationFieldPath =
578
600
  (this.parent as any)?.fieldPath ??
579
601
  (this.parent as any)?.context?.fieldPath ??
@@ -592,9 +614,11 @@ export class SubTableColumnModel<
592
614
  }
593
615
  fork.context.defineProperty('item', {
594
616
  get: () => {
617
+ const form = (fork.context as any)?.form || (this.context?.blockModel as any)?.context?.form;
618
+ const rowRecord = getLatestSubTableRowRecord(form, fork.context.fieldIndex, record);
595
619
  const parentItemCtx = (parentItem ?? this.context?.item) as any;
596
- const isNew = record?.__is_new__;
597
- const isStored = record?.__is_stored__;
620
+ const isNew = rowRecord?.__is_new__;
621
+ const isStored = rowRecord?.__is_stored__;
598
622
  const list = (this.parent as any)?.props?.value;
599
623
  const length = Array.isArray(list) ? list.length : undefined;
600
624
  return {
@@ -602,7 +626,7 @@ export class SubTableColumnModel<
602
626
  length,
603
627
  __is_new__: isNew,
604
628
  __is_stored__: isStored,
605
- value: record,
629
+ value: rowRecord,
606
630
  parentItem: parentItemCtx,
607
631
  };
608
632
  },