@nocobase/client-v2 2.1.0-alpha.24 → 2.1.0-alpha.26

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.
@@ -17,6 +17,7 @@ import { namePathToPathKey, parsePathString, pathKeyToNamePath } from './path';
17
17
  import type { FormAssignRuleItem, FormValueWriteMeta, NamePath, Patch, SetOptions, ValueSource } from './types';
18
18
  import { createTxId, isEmptyValue } from './utils';
19
19
  import { isToManyAssociationField } from '../../../../internal/utils/modelUtils';
20
+ import { getSubTableRowIdentity } from '../../../fields/AssociationFieldModel/SubTableFieldModel/rowIdentity';
20
21
 
21
22
  /** Symbol to indicate rule value resolution should be skipped */
22
23
  const SKIP_RULE_VALUE = Symbol('SKIP_RULE_VALUE');
@@ -51,6 +52,7 @@ type RuntimeRule = {
51
52
 
52
53
  type ObservableBinding = {
53
54
  source: ValueSource;
55
+ pathKey: string;
54
56
  dispose: () => void;
55
57
  };
56
58
 
@@ -1172,7 +1174,6 @@ export class RuleEngine {
1172
1174
 
1173
1175
  const ruleContext = this.prepareRuleContext(rule);
1174
1176
  const { baseCtx, targetNamePath, targetKey, clearDeps, disposeBinding } = ruleContext;
1175
-
1176
1177
  if (!this.shouldRunRule(rule, targetNamePath, targetKey, baseCtx)) {
1177
1178
  clearDeps(state);
1178
1179
  disposeBinding();
@@ -1487,18 +1488,59 @@ export class RuleEngine {
1487
1488
  // 编辑态默认值规则:
1488
1489
  // - 顶层编辑表单:不应用默认值
1489
1490
  // - 子表单:仅对“新增行/新增对象”(__is_new__ = true)应用默认值
1490
- let item: any;
1491
+ return this.getItemContext(baseCtx)?.__is_new__ === true;
1492
+ }
1493
+
1494
+ private isStaleToManyItemContext(baseCtx: any): boolean {
1495
+ const item = this.getItemContext(baseCtx);
1496
+ if (!Number.isFinite(item?.index) || !Number.isFinite(item?.length)) return false;
1497
+ return item.index < 0 || item.index >= item.length;
1498
+ }
1499
+
1500
+ private getItemContext(baseCtx: any) {
1491
1501
  try {
1492
- item = baseCtx?.item;
1502
+ return baseCtx?.item;
1493
1503
  } catch {
1494
- item = undefined;
1504
+ return undefined;
1495
1505
  }
1506
+ }
1496
1507
 
1497
- if (!item || typeof item !== 'object') {
1498
- return false;
1508
+ private getRowTargetKey(baseCtx: any, rowPath: NamePath): string | string[] {
1509
+ let collection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
1510
+ let field: any;
1511
+ for (const seg of rowPath) {
1512
+ if (typeof seg === 'number') continue;
1513
+ if (typeof seg !== 'string' || !seg || !collection?.getField) break;
1514
+
1515
+ field = collection?.getField?.(seg);
1516
+ if (!field?.isAssociationField?.()) break;
1517
+ collection = field?.targetCollection;
1499
1518
  }
1500
1519
 
1501
- return item.__is_new__ === true;
1520
+ const raw = field?.targetCollection?.filterTargetKey ?? field?.targetCollection?.filterByTk ?? field?.targetKey;
1521
+ if (Array.isArray(raw)) {
1522
+ const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
1523
+ return keys.length ? keys : 'id';
1524
+ }
1525
+ return typeof raw === 'string' && raw ? raw : 'id';
1526
+ }
1527
+
1528
+ private isMismatchedToManyItemContext(baseCtx: any, targetNamePath: NamePath): boolean {
1529
+ const item = this.getItemContext(baseCtx);
1530
+ if (!item) return false;
1531
+
1532
+ for (let i = targetNamePath.length - 1; i >= 0; i--) {
1533
+ if (typeof targetNamePath[i] !== 'number') continue;
1534
+
1535
+ const rowPath = targetNamePath.slice(0, i + 1);
1536
+ const targetKey = this.getRowTargetKey(baseCtx, rowPath);
1537
+ const currentRow = this.options.getFormValueAtPath(rowPath);
1538
+ const currentIdentity = getSubTableRowIdentity(currentRow, targetKey);
1539
+ const itemIdentity = getSubTableRowIdentity(item.value, targetKey);
1540
+ return !!currentIdentity && !!itemIdentity && currentIdentity !== itemIdentity;
1541
+ }
1542
+
1543
+ return false;
1502
1544
  }
1503
1545
 
1504
1546
  private shouldRunRule(
@@ -1509,6 +1551,8 @@ export class RuleEngine {
1509
1551
  ): boolean {
1510
1552
  if (!rule.getEnabled()) return false;
1511
1553
  if (!targetNamePath || !targetKey) return false;
1554
+ if (this.isStaleToManyItemContext(baseCtx)) return false;
1555
+ if (this.isMismatchedToManyItemContext(baseCtx, targetNamePath)) return false;
1512
1556
  if (rule.source === 'default') {
1513
1557
  if (!this.shouldApplyDefaultRuleInCurrentState(baseCtx)) return false;
1514
1558
  if (this.options.findExplicitHit(targetKey)) return false;
@@ -1808,6 +1852,19 @@ export class RuleEngine {
1808
1852
  }
1809
1853
  })();
1810
1854
 
1855
+ // Row/grid rules resolve target paths through fieldIndex (for example `roles.title`
1856
+ // -> `roles[1].title`). When a row is deleted or reordered, the rule must reschedule
1857
+ // even if its value/condition does not reference ctx.item directly.
1858
+ const fieldIndex = baseCtx?.model?.context?.fieldIndex ?? baseCtx?.fieldIndex;
1859
+ const shouldWatchFieldIndex = Array.isArray(fieldIndex) && fieldIndex.some((it) => typeof it === 'string');
1860
+ if (shouldWatchFieldIndex) {
1861
+ const fieldIndexDisposer = reaction(
1862
+ () => this.getFieldIndexSignature(baseCtx),
1863
+ () => this.scheduleRule(rule.id),
1864
+ );
1865
+ state.depDisposers.push(fieldIndexDisposer);
1866
+ }
1867
+
1811
1868
  for (const depKey of deps) {
1812
1869
  if (depKey === 'fv:*') {
1813
1870
  continue;
@@ -1841,14 +1898,9 @@ export class RuleEngine {
1841
1898
  const subPath = sep >= 0 ? rest.slice(sep + 1) : '';
1842
1899
  const depPath = subPath ? (parsePathString(subPath).filter((seg) => typeof seg !== 'object') as NamePath) : [];
1843
1900
 
1844
- // 特殊变量:item 为 RuleEngine 注入的计算属性(不直接存在于 baseCtx 上),其 parentItem/index 链依赖 fieldIndex。
1901
+ // 特殊变量:item 为 RuleEngine 注入的计算属性(不直接存在于 baseCtx 上)。
1902
+ // fieldIndex 的变化已统一在上面监听,这里只补 item 自身取值的依赖。
1845
1903
  if (varName === 'item') {
1846
- const fieldIndexDisposer = reaction(
1847
- () => this.getFieldIndexSignature(baseCtx),
1848
- () => this.scheduleRule(rule.id),
1849
- );
1850
- state.depDisposers.push(fieldIndexDisposer);
1851
-
1852
1904
  if (depPath.length) {
1853
1905
  const trackingFormValues = this.options.createTrackingFormValues({ deps: new Set(), wildcard: false });
1854
1906
  const itemValueDisposer = reaction(
@@ -16,6 +16,7 @@ import { createFormValuesProxy } from './deps';
16
16
  import { createFormPatcher } from './form-patch';
17
17
  import { namePathToPathKey, pathKeyToNamePath, resolveDynamicNamePath } from './path';
18
18
  import { RuleEngine } from './rules';
19
+ import { getSubTableRowIdentity } from '../../../fields/AssociationFieldModel/SubTableFieldModel/rowIdentity';
19
20
  import type {
20
21
  FormAssignRuleItem,
21
22
  FormValueWriteMeta,
@@ -25,10 +26,11 @@ import type {
25
26
  SetOptions,
26
27
  ValueSource,
27
28
  } from './types';
28
- import { createTxId, MAX_WRITES_PER_PATH_PER_TX } from './utils';
29
+ import { createTxId, isEmptyValue, MAX_WRITES_PER_PATH_PER_TX } from './utils';
29
30
 
30
31
  type ObservableBinding = {
31
32
  source: ValueSource;
33
+ pathKey: string;
32
34
  dispose: () => void;
33
35
  };
34
36
 
@@ -241,6 +243,7 @@ export class FormValueRuntime {
241
243
 
242
244
  const changedPaths: NamePath[] = [];
243
245
  const touchedChangedPathKeys = new Set<string>();
246
+ let hasMeaningfulTouchedChange = false;
244
247
  let bumpedWriteSeq = false;
245
248
  for (const field of changedFields || []) {
246
249
  const name = field?.name;
@@ -259,8 +262,11 @@ export class FormValueRuntime {
259
262
  }
260
263
  _.set(this.valuesMirror, namePath, nextValue);
261
264
  changedPaths.push(namePath);
262
- if (field?.touched === true) {
265
+ const isMeaningfulTouched =
266
+ field?.touched === true && !this.shouldIgnoreSyntheticTouchedInit(namePath, prevValue, nextValue);
267
+ if (isMeaningfulTouched) {
263
268
  touchedChangedPathKeys.add(namePathToPathKey(namePath));
269
+ hasMeaningfulTouchedChange = true;
264
270
  }
265
271
  }
266
272
 
@@ -279,7 +285,7 @@ export class FormValueRuntime {
279
285
  return;
280
286
  }
281
287
 
282
- const isUser = changedFields.some((f) => f?.touched === true);
288
+ const isUser = hasMeaningfulTouchedChange;
283
289
  const source: ValueSource = isUser ? 'user' : 'system';
284
290
 
285
291
  const writeSeq = this.writeSeq;
@@ -299,16 +305,267 @@ export class FormValueRuntime {
299
305
  });
300
306
  }
301
307
 
308
+ private shouldIgnoreSyntheticTouchedInit(namePath: NamePath, prevValue: any, nextValue: any) {
309
+ if (!namePath?.length) return false;
310
+ if (typeof prevValue !== 'undefined') return false;
311
+ if (!isEmptyValue(nextValue)) return false;
312
+
313
+ for (let i = namePath.length - 1; i >= 0; i--) {
314
+ if (typeof namePath[i] !== 'number') continue;
315
+ const rowPath = namePath.slice(0, i + 1);
316
+ const rowValue = this.getFormValueAtPath(rowPath);
317
+ return !!rowValue?.__is_new__;
318
+ }
319
+
320
+ return false;
321
+ }
322
+
323
+ private isDeletedArrayItemPath(path: NamePath | string, snapshot: any) {
324
+ const namePath = Array.isArray(path) ? path : pathKeyToNamePath(path);
325
+ if (!namePath?.length) return false;
326
+
327
+ for (let i = namePath.length - 1; i >= 0; i--) {
328
+ if (typeof namePath[i] !== 'number') continue;
329
+ const rowPath = namePath.slice(0, i + 1);
330
+ return typeof _.get(snapshot, rowPath as any) === 'undefined';
331
+ }
332
+
333
+ return false;
334
+ }
335
+
336
+ private pruneDeletedArrayItemState(snapshot: any) {
337
+ for (const key of Array.from(this.explicitSet)) {
338
+ if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
339
+ this.explicitSet.delete(key);
340
+ }
341
+
342
+ for (const key of Array.from(this.lastDefaultValueByPathKey.keys())) {
343
+ if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
344
+ this.lastDefaultValueByPathKey.delete(key);
345
+ }
346
+
347
+ for (const key of Array.from(this.lastWriteMetaByPathKey.keys())) {
348
+ if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
349
+ this.lastWriteMetaByPathKey.delete(key);
350
+ }
351
+
352
+ for (const [key, binding] of Array.from(this.observableBindings.entries())) {
353
+ if (!this.isDeletedArrayItemPath(key, snapshot)) continue;
354
+ binding.dispose();
355
+ this.observableBindings.delete(key);
356
+ }
357
+ }
358
+
359
+ private getArrayItemTargetKey(arrayPath?: NamePath): string | string[] {
360
+ let collection = this.model?.context?.collection;
361
+ let field: any;
362
+ for (const seg of arrayPath || []) {
363
+ if (typeof seg === 'number') continue;
364
+ if (typeof seg !== 'string' || !collection?.getField) break;
365
+
366
+ field = collection?.getField?.(seg);
367
+ if (!field?.isAssociationField?.()) break;
368
+ collection = field?.targetCollection;
369
+ }
370
+
371
+ const raw = field?.targetCollection?.filterTargetKey ?? field?.targetCollection?.filterByTk ?? field?.targetKey;
372
+ if (Array.isArray(raw)) {
373
+ const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
374
+ return keys.length ? keys : 'id';
375
+ }
376
+ return typeof raw === 'string' && raw ? raw : 'id';
377
+ }
378
+
379
+ private getArrayItemIdentity(item: any, arrayPath?: NamePath) {
380
+ return getSubTableRowIdentity(item, this.getArrayItemTargetKey(arrayPath));
381
+ }
382
+
383
+ private reconcileArrayItemState(rawChangedPaths: NamePath[], changedValues: any, snapshot: any) {
384
+ const seenPathKeys = new Set<string>();
385
+
386
+ for (const path of rawChangedPaths || []) {
387
+ if (!path?.length) continue;
388
+ const pathKey = namePathToPathKey(path);
389
+ if (seenPathKeys.has(pathKey)) continue;
390
+ seenPathKeys.add(pathKey);
391
+
392
+ const prevValue = _.get(this.valuesMirror, path as any);
393
+ const nextValue = this.getObservedChangedValue(path, changedValues, snapshot);
394
+ if (!Array.isArray(prevValue) || !Array.isArray(nextValue)) continue;
395
+
396
+ const nextIndexByIdentity = new Map<string, number>();
397
+ nextValue.forEach((item, index) => {
398
+ const identity = this.getArrayItemIdentity(item, path);
399
+ if (identity) {
400
+ nextIndexByIdentity.set(identity, index);
401
+ }
402
+ });
403
+
404
+ if (!nextIndexByIdentity.size) continue;
405
+
406
+ this.reconcileArrayItemSet(this.explicitSet, path, prevValue, nextIndexByIdentity);
407
+ this.reconcileArrayItemMap(this.lastDefaultValueByPathKey, path, prevValue, nextIndexByIdentity);
408
+ this.reconcileArrayItemMap(this.lastWriteMetaByPathKey, path, prevValue, nextIndexByIdentity);
409
+ this.reconcileObservableBindings(path, prevValue, nextIndexByIdentity);
410
+ }
411
+ }
412
+
413
+ private getReconciledArrayItemPath(
414
+ pathKey: string,
415
+ arrayPath: NamePath,
416
+ prevItems: any[],
417
+ nextIndexByIdentity: Map<string, number>,
418
+ ) {
419
+ const namePath = pathKeyToNamePath(pathKey);
420
+ if (namePath.length <= arrayPath.length) return { action: 'keep' as const };
421
+
422
+ for (let i = 0; i < arrayPath.length; i++) {
423
+ if (namePath[i] !== arrayPath[i]) return { action: 'keep' as const };
424
+ }
425
+
426
+ const oldIndex = namePath[arrayPath.length];
427
+ if (typeof oldIndex !== 'number') return { action: 'keep' as const };
428
+
429
+ const identity = this.getArrayItemIdentity(prevItems[oldIndex], arrayPath);
430
+ if (!identity) return { action: 'keep' as const };
431
+
432
+ const nextIndex = nextIndexByIdentity.get(identity);
433
+ if (nextIndex == null) return { action: 'delete' as const };
434
+ if (nextIndex === oldIndex) return { action: 'keep' as const };
435
+
436
+ const nextPath = [...namePath];
437
+ nextPath[arrayPath.length] = nextIndex;
438
+ return { action: 'move' as const, key: namePathToPathKey(nextPath) };
439
+ }
440
+
441
+ private reconcileArrayItemSet(
442
+ target: Set<string>,
443
+ arrayPath: NamePath,
444
+ prevItems: any[],
445
+ nextIndexByIdentity: Map<string, number>,
446
+ ) {
447
+ const nextEntries = new Set<string>();
448
+ let changed = false;
449
+
450
+ for (const key of target) {
451
+ const result = this.getReconciledArrayItemPath(key, arrayPath, prevItems, nextIndexByIdentity);
452
+ if (result.action === 'delete') {
453
+ changed = true;
454
+ continue;
455
+ }
456
+ if (result.action === 'move') {
457
+ changed = true;
458
+ nextEntries.add(result.key);
459
+ continue;
460
+ }
461
+ nextEntries.add(key);
462
+ }
463
+
464
+ if (!changed) return;
465
+ target.clear();
466
+ for (const key of nextEntries) {
467
+ target.add(key);
468
+ }
469
+ }
470
+
471
+ private reconcileArrayItemMap<T>(
472
+ target: Map<string, T>,
473
+ arrayPath: NamePath,
474
+ prevItems: any[],
475
+ nextIndexByIdentity: Map<string, number>,
476
+ ) {
477
+ const nextEntries = new Map<string, T>();
478
+ let changed = false;
479
+
480
+ for (const [key, value] of target) {
481
+ const result = this.getReconciledArrayItemPath(key, arrayPath, prevItems, nextIndexByIdentity);
482
+ if (result.action === 'delete') {
483
+ changed = true;
484
+ continue;
485
+ }
486
+ if (result.action === 'move') {
487
+ changed = true;
488
+ nextEntries.set(result.key, value);
489
+ continue;
490
+ }
491
+ nextEntries.set(key, value);
492
+ }
493
+
494
+ if (!changed) return;
495
+ target.clear();
496
+ for (const [key, value] of nextEntries) {
497
+ target.set(key, value);
498
+ }
499
+ }
500
+
501
+ private reconcileObservableBindings(arrayPath: NamePath, prevItems: any[], nextIndexByIdentity: Map<string, number>) {
502
+ const nextEntries = new Map<string, ObservableBinding>();
503
+ let changed = false;
504
+
505
+ for (const [key, binding] of this.observableBindings) {
506
+ const result = this.getReconciledArrayItemPath(key, arrayPath, prevItems, nextIndexByIdentity);
507
+ if (result.action === 'delete') {
508
+ changed = true;
509
+ binding.dispose();
510
+ continue;
511
+ }
512
+ if (result.action === 'move') {
513
+ changed = true;
514
+ binding.pathKey = result.key;
515
+ nextEntries.set(result.key, binding);
516
+ continue;
517
+ }
518
+ nextEntries.set(key, binding);
519
+ }
520
+
521
+ if (!changed) return;
522
+ this.observableBindings.clear();
523
+ for (const [key, binding] of nextEntries) {
524
+ this.observableBindings.set(key, binding);
525
+ }
526
+ }
527
+
528
+ private getObservedChangedValue(path: NamePath, changedValues: any, snapshot: any) {
529
+ if (path?.length === 1) {
530
+ const key = path[0];
531
+ if (
532
+ (typeof key === 'string' || typeof key === 'number') &&
533
+ changedValues &&
534
+ Object.prototype.hasOwnProperty.call(changedValues, key)
535
+ ) {
536
+ return changedValues[key];
537
+ }
538
+ }
539
+
540
+ return _.get(snapshot, path as any);
541
+ }
542
+
543
+ private getObservedSnapshot(changedValues: any, snapshot: any) {
544
+ if (!changedValues || typeof changedValues !== 'object') return snapshot;
545
+ if (!snapshot || typeof snapshot !== 'object') return changedValues;
546
+
547
+ let observed = snapshot;
548
+ for (const key of Object.keys(changedValues)) {
549
+ if (observed === snapshot) {
550
+ observed = Array.isArray(snapshot) ? [...snapshot] : { ...snapshot };
551
+ }
552
+ _.set(observed, [key], changedValues[key]);
553
+ }
554
+
555
+ return observed;
556
+ }
557
+
302
558
  handleFormValuesChange(changedValues: any, allValues: any) {
303
559
  if (this.disposed) return;
304
560
  if (this.isSuppressed()) {
305
561
  return;
306
562
  }
307
563
 
564
+ const changedValuePaths: NamePath[] = Object.keys(changedValues || {}).map((k) => [k]);
308
565
  const rawChangedPaths: NamePath[] =
309
566
  this.lastObservedChangedPaths && this.lastObservedChangedPaths.length
310
567
  ? this.lastObservedChangedPaths
311
- : Object.keys(changedValues || {}).map((k) => [k]);
568
+ : changedValuePaths;
312
569
 
313
570
  const source: ValueSource = this.lastObservedSource ?? 'user';
314
571
 
@@ -316,16 +573,21 @@ export class FormValueRuntime {
316
573
  this.lastObservedChangedPaths = null;
317
574
  this.lastObservedSource = null;
318
575
 
319
- const snapshot = allValues && typeof allValues === 'object' ? allValues : this.getFormValuesSnapshot();
576
+ const rawSnapshot = allValues && typeof allValues === 'object' ? allValues : this.getFormValuesSnapshot();
577
+ const snapshot = this.getObservedSnapshot(changedValues, rawSnapshot);
578
+ this.reconcileArrayItemState([...rawChangedPaths, ...changedValuePaths], changedValues, snapshot);
579
+ this.pruneDeletedArrayItemState(snapshot);
320
580
 
321
- const explicitPaths = this.deriveExplicitPaths(rawChangedPaths, snapshot, this.valuesMirror);
581
+ const explicitPaths = this.deriveExplicitPaths(rawChangedPaths, changedValues, snapshot, this.valuesMirror).filter(
582
+ (path) => !this.isDeletedArrayItemPath(path, snapshot),
583
+ );
322
584
 
323
585
  let hasMirrorChange = false;
324
586
  let bumpedWriteSeq = false;
325
587
  const actuallyChangedPaths: NamePath[] = [];
326
588
  for (const p of rawChangedPaths) {
327
589
  if (!p?.length) continue;
328
- const nextValue = _.get(snapshot, p);
590
+ const nextValue = this.getObservedChangedValue(p, changedValues, snapshot);
329
591
  const prevValue = _.get(this.valuesMirror, p);
330
592
  if (_.isEqual(prevValue, nextValue)) continue;
331
593
  if (!bumpedWriteSeq) {
@@ -364,14 +626,14 @@ export class FormValueRuntime {
364
626
  });
365
627
  }
366
628
 
367
- private deriveExplicitPaths(rawChangedPaths: NamePath[], snapshot: any, valuesMirrorBefore: any): NamePath[] {
629
+ private deriveExplicitPaths(rawChangedPaths: NamePath[], changedValues: any, snapshot: any, valuesMirrorBefore: any) {
368
630
  const explicitPaths: NamePath[] = [];
369
631
  const seen = new Set<string>();
370
632
 
371
633
  for (const path of rawChangedPaths || []) {
372
634
  if (!path?.length) continue;
373
635
  const prevValue = _.get(valuesMirrorBefore, path as any);
374
- const nextValue = _.get(snapshot, path as any);
636
+ const nextValue = this.getObservedChangedValue(path, changedValues, snapshot);
375
637
  this.collectExplicitDiffPaths(prevValue, nextValue, path, explicitPaths, seen);
376
638
  }
377
639
 
@@ -667,19 +929,30 @@ export class FormValueRuntime {
667
929
  }
668
930
  }
669
931
 
670
- for (const { namePath, rawValue, pathKey } of filteredToWrite) {
932
+ for (const { rawValue, pathKey } of filteredToWrite) {
671
933
  if (!isObservable(rawValue)) continue;
672
934
  const obs = rawValue;
673
935
 
936
+ const binding: ObservableBinding = { source, pathKey, dispose: () => {} };
674
937
  const disposer = observe(obs, () => {
675
- this.applyBoundValue(callerCtx, namePath, pathKey, toJS(obs), source, linkageScopeDepth, linkageTxId);
938
+ const currentPathKey = binding.pathKey;
939
+ this.applyBoundValue(
940
+ callerCtx,
941
+ pathKeyToNamePath(currentPathKey),
942
+ currentPathKey,
943
+ toJS(obs),
944
+ source,
945
+ linkageScopeDepth,
946
+ linkageTxId,
947
+ );
676
948
  });
949
+ binding.dispose = disposer;
677
950
 
678
951
  const existing = this.observableBindings.get(pathKey);
679
952
  if (existing) {
680
953
  existing.dispose();
681
954
  }
682
- this.observableBindings.set(pathKey, { source, dispose: disposer });
955
+ this.observableBindings.set(pathKey, binding);
683
956
  }
684
957
 
685
958
  if (triggerEvent) {
@@ -34,6 +34,7 @@ import { SubTableFieldModel } from '.';
34
34
  import { FieldModel } from '../../../base/FieldModel';
35
35
  import { FieldDeletePlaceholder, CustomWidth } from '../../../blocks/table/TableColumnModel';
36
36
  import { buildDynamicNamePath } from '../../../blocks/form/dynamicNamePath';
37
+ import { getSubTableRowIdentity } from './rowIdentity';
37
38
 
38
39
  const SubTableRowRuleBinder: React.FC<{ model: any }> = ({ model }) => {
39
40
  React.useEffect(() => {
@@ -355,7 +356,11 @@ const MemoCell: React.FC<CellProps> = React.memo(
355
356
  },
356
357
  (prev, next) => {
357
358
  return (
358
- prev.value === next.value && prev.id === next.id && prev.memoKey === next.memoKey && prev.width === next.width
359
+ prev.value === next.value &&
360
+ prev.id === next.id &&
361
+ prev.memoKey === next.memoKey &&
362
+ prev.width === next.width &&
363
+ prev.rowIdx === next.rowIdx
359
364
  );
360
365
  },
361
366
  );
@@ -548,7 +553,10 @@ export class SubTableColumnModel<
548
553
  const baseFieldIndex = parentFieldIndex ?? (this.parent as any)?.context?.fieldIndex ?? this.context?.fieldIndex;
549
554
  const baseArr = Array.isArray(baseFieldIndex) ? baseFieldIndex : [];
550
555
  const baseIndexKey = baseArr.length ? baseArr.join('|') : 'root';
551
- const rowForkKey = `row:${baseIndexKey}:${String(rowIdx)}`;
556
+ const filterTargetKey =
557
+ (this.parent as any)?.collection?.filterTargetKey ?? (this.parent as any)?.context?.collection?.filterTargetKey;
558
+ const rowIdentity = getSubTableRowIdentity(record, filterTargetKey) ?? `row:${String(rowIdx)}`;
559
+ const rowForkKey = `row:${baseIndexKey}:${rowIdentity}:${String(rowIdx)}`;
552
560
  const rowFork: any = (() => {
553
561
  const fork = this.createFork({}, rowForkKey);
554
562
  const associationFieldPath =