@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.
- package/es/flow/models/base/BlockGridModel.d.ts +2 -1
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +7 -1
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +5 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +12 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.d.ts +18 -0
- package/es/index.mjs +84 -84
- package/lib/index.js +84 -84
- package/package.json +5 -5
- package/src/flow/models/base/BlockGridModel.tsx +48 -2
- package/src/flow/models/base/__tests__/BlockGridModel.selectSceneAddBlock.test.ts +83 -0
- package/src/flow/models/blocks/form/QuickEditFormModel.tsx +39 -16
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +293 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/subtable-nested.test.ts +3 -2
- package/src/flow/models/blocks/form/value-runtime/rules.ts +66 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +285 -12
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +46 -22
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/rowIdentity.ts +70 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/SubTableRowIdentity.test.ts +45 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
1502
|
+
return baseCtx?.item;
|
|
1493
1503
|
} catch {
|
|
1494
|
-
|
|
1504
|
+
return undefined;
|
|
1495
1505
|
}
|
|
1506
|
+
}
|
|
1496
1507
|
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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
|
|
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 =
|
|
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)
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
955
|
+
this.observableBindings.set(pathKey, binding);
|
|
683
956
|
}
|
|
684
957
|
|
|
685
958
|
if (triggerEvent) {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -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 &&
|
|
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
|
|
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 =
|