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

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 (42) hide show
  1. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
  2. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
  3. package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
  4. package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
  5. package/es/flow/utils/formValueDeps.d.ts +32 -0
  6. package/es/index.mjs +113 -93
  7. package/lib/index.js +104 -84
  8. package/package.json +7 -7
  9. package/src/components/AppComponents.tsx +19 -3
  10. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
  11. package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
  12. package/src/flow/actions/dateTimeFormat.tsx +42 -28
  13. package/src/flow/actions/linkageRules.tsx +1 -1
  14. package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
  15. package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
  16. package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
  17. package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
  18. package/src/flow/flows/editMarkdownFlow.tsx +1 -1
  19. package/src/flow/index.ts +1 -1
  20. package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
  21. package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
  22. package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
  23. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
  24. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
  25. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
  26. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -0
  27. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
  28. package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
  29. package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
  30. package/src/flow/models/fields/InputFieldModel.tsx +48 -2
  31. package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
  32. package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
  33. package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
  34. package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
  35. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
  36. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
  37. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
  38. package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
  39. package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
  40. package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
  41. package/src/flow/utils/formValueDeps.ts +170 -0
  42. package/src/settings-center/AdminSettingsLayout.tsx +7 -2
@@ -93,4 +93,1026 @@ describe('ensureFormValueDrivenDataScopeClear', () => {
93
93
 
94
94
  expect(onChange).not.toHaveBeenCalled();
95
95
  });
96
+
97
+ it('maps ctx.item.value dependencies to the current row path', () => {
98
+ const emitter = new EventEmitter();
99
+ const formBlock = {
100
+ uid: 'form-1',
101
+ disposed: false,
102
+ emitter,
103
+ context: { form: {} },
104
+ };
105
+
106
+ const onChange = vi.fn();
107
+ const model: any = {
108
+ disposed: false,
109
+ props: {
110
+ value: { id: 1 },
111
+ onChange,
112
+ },
113
+ context: {
114
+ blockModel: formBlock,
115
+ fieldIndex: ['users:1'],
116
+ },
117
+ };
118
+
119
+ const ctx: any = {
120
+ model,
121
+ flowKey: 'selectSettings',
122
+ };
123
+
124
+ const filter = {
125
+ logic: '$and',
126
+ items: [{ path: 'nickname', operator: '$eq', value: '{{ ctx.item.value.nickname }}' }],
127
+ };
128
+
129
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
130
+
131
+ emitter.emit('formValuesChange', {
132
+ changedPaths: [['users', 0, 'nickname']],
133
+ });
134
+ expect(onChange).not.toHaveBeenCalled();
135
+
136
+ emitter.emit('formValuesChange', {
137
+ changedPaths: [['users', 1, 'nickname']],
138
+ });
139
+ expect(onChange).toHaveBeenCalledWith(null);
140
+ });
141
+
142
+ it('parses object-patch changedPaths before matching ctx.item.value dependencies', () => {
143
+ const emitter = new EventEmitter();
144
+ const formBlock = {
145
+ uid: 'form-1',
146
+ disposed: false,
147
+ emitter,
148
+ context: { form: {} },
149
+ };
150
+
151
+ const onChange = vi.fn();
152
+ const model: any = {
153
+ disposed: false,
154
+ props: {
155
+ value: { id: 1 },
156
+ onChange,
157
+ },
158
+ context: {
159
+ blockModel: formBlock,
160
+ fieldIndex: ['users:1'],
161
+ },
162
+ };
163
+
164
+ const ctx: any = {
165
+ model,
166
+ flowKey: 'selectSettings',
167
+ };
168
+
169
+ const filter = {
170
+ logic: '$and',
171
+ items: [{ path: 'nickname', operator: '$eq', value: '{{ ctx.item.value.nickname }}' }],
172
+ };
173
+
174
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
175
+
176
+ emitter.emit('formValuesChange', {
177
+ changedPaths: [['users[1].nickname']],
178
+ });
179
+
180
+ expect(onChange).toHaveBeenCalledWith(null);
181
+ });
182
+
183
+ it('maps root parentItem.value dependencies to the root form path', () => {
184
+ const emitter = new EventEmitter();
185
+ const formBlock = {
186
+ uid: 'form-1',
187
+ disposed: false,
188
+ emitter,
189
+ context: { form: {} },
190
+ };
191
+
192
+ const onChange = vi.fn();
193
+ const model: any = {
194
+ disposed: false,
195
+ props: {
196
+ value: { id: 1 },
197
+ onChange,
198
+ },
199
+ context: {
200
+ blockModel: formBlock,
201
+ fieldIndex: ['roles:0'],
202
+ },
203
+ };
204
+
205
+ const ctx: any = {
206
+ model,
207
+ flowKey: 'selectSettings',
208
+ };
209
+
210
+ const filter = {
211
+ logic: '$and',
212
+ items: [{ path: 'departmentId', operator: '$eq', value: '{{ ctx.item.parentItem.value.departmentId }}' }],
213
+ };
214
+
215
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
216
+
217
+ emitter.emit('formValuesChange', {
218
+ changedPaths: [['roles', 0, 'name']],
219
+ });
220
+ expect(onChange).not.toHaveBeenCalled();
221
+
222
+ emitter.emit('formValuesChange', {
223
+ changedPaths: [['departmentId']],
224
+ });
225
+ expect(onChange).toHaveBeenCalledWith(null);
226
+ });
227
+
228
+ it('does not clear a row field when a sibling field changes but the item dependency value is unchanged', () => {
229
+ const emitter = new EventEmitter();
230
+ const formBlock = {
231
+ uid: 'form-1',
232
+ disposed: false,
233
+ emitter,
234
+ context: {
235
+ form: {},
236
+ formValues: {
237
+ org_o2m: [{ companyName: 'a', user_m2o: { id: 1 }, staff_m2o: null }],
238
+ },
239
+ },
240
+ };
241
+
242
+ const onChange = vi.fn();
243
+ const model: any = {
244
+ disposed: false,
245
+ props: {
246
+ value: { id: 1 },
247
+ onChange,
248
+ },
249
+ context: {
250
+ blockModel: formBlock,
251
+ fieldIndex: ['org_o2m:0'],
252
+ },
253
+ };
254
+
255
+ const ctx: any = {
256
+ model,
257
+ flowKey: 'selectSettings',
258
+ };
259
+
260
+ const filter = {
261
+ logic: '$and',
262
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
263
+ };
264
+
265
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
266
+
267
+ emitter.emit('formValuesChange', {
268
+ changedPaths: [['org_o2m', 0]],
269
+ allValues: {
270
+ org_o2m: [{ companyName: 'a', user_m2o: { id: 1 }, staff_m2o: { id: 10 } }],
271
+ },
272
+ });
273
+
274
+ expect(onChange).not.toHaveBeenCalled();
275
+ });
276
+
277
+ it('clears a row field when a coarse row change includes an actual item dependency value change', () => {
278
+ const emitter = new EventEmitter();
279
+ const formBlock = {
280
+ uid: 'form-1',
281
+ disposed: false,
282
+ emitter,
283
+ context: {
284
+ form: {},
285
+ formValues: {
286
+ org_o2m: [{ companyName: 'a', user_m2o: { id: 1 } }],
287
+ },
288
+ },
289
+ };
290
+
291
+ const onChange = vi.fn();
292
+ const model: any = {
293
+ disposed: false,
294
+ props: {
295
+ value: { id: 1 },
296
+ onChange,
297
+ },
298
+ context: {
299
+ blockModel: formBlock,
300
+ fieldIndex: ['org_o2m:0'],
301
+ },
302
+ };
303
+
304
+ const ctx: any = {
305
+ model,
306
+ flowKey: 'selectSettings',
307
+ };
308
+
309
+ const filter = {
310
+ logic: '$and',
311
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
312
+ };
313
+
314
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
315
+
316
+ emitter.emit('formValuesChange', {
317
+ changedPaths: [['org_o2m', 0]],
318
+ allValues: {
319
+ org_o2m: [{ companyName: 'b', user_m2o: { id: 1 } }],
320
+ },
321
+ });
322
+
323
+ expect(onChange).toHaveBeenCalledWith(null);
324
+ });
325
+
326
+ it('clears a dependent row field when the sibling relation used by data scope changes', () => {
327
+ const emitter = new EventEmitter();
328
+ const formBlock = {
329
+ uid: 'form-1',
330
+ disposed: false,
331
+ emitter,
332
+ context: {
333
+ form: {},
334
+ formValues: {
335
+ org_o2m: [{ companyName: 'a', user_m2o: { id: 1 }, staff_m2o: { id: 10 } }],
336
+ },
337
+ },
338
+ };
339
+
340
+ const onChange = vi.fn();
341
+ const model: any = {
342
+ disposed: false,
343
+ props: {
344
+ value: { id: 10 },
345
+ onChange,
346
+ },
347
+ context: {
348
+ blockModel: formBlock,
349
+ fieldIndex: ['org_o2m:0'],
350
+ },
351
+ };
352
+
353
+ const ctx: any = {
354
+ model,
355
+ flowKey: 'selectSettings',
356
+ };
357
+
358
+ const filter = {
359
+ logic: '$and',
360
+ items: [{ path: 'id', operator: '$eq', value: '{{ ctx.item.value.user_m2o.id }}' }],
361
+ };
362
+
363
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
364
+
365
+ emitter.emit('formValuesChange', {
366
+ changedPaths: [['org_o2m', 0]],
367
+ allValues: {
368
+ org_o2m: [{ companyName: 'a', user_m2o: { id: 2 }, staff_m2o: { id: 10 } }],
369
+ },
370
+ });
371
+
372
+ expect(onChange).toHaveBeenCalledWith(null);
373
+ });
374
+
375
+ it('does not clear a row field when another row is removed after it', () => {
376
+ const emitter = new EventEmitter();
377
+ const formBlock = {
378
+ uid: 'form-1',
379
+ disposed: false,
380
+ emitter,
381
+ context: {
382
+ form: {},
383
+ formValues: {
384
+ org_o2m: [
385
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
386
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
387
+ ],
388
+ },
389
+ },
390
+ };
391
+
392
+ const onChange = vi.fn();
393
+ const model: any = {
394
+ disposed: false,
395
+ props: {
396
+ value: { id: 1 },
397
+ onChange,
398
+ },
399
+ context: {
400
+ blockModel: formBlock,
401
+ fieldIndex: ['org_o2m:0'],
402
+ },
403
+ };
404
+
405
+ const ctx: any = {
406
+ model,
407
+ flowKey: 'selectSettings',
408
+ };
409
+
410
+ const filter = {
411
+ logic: '$and',
412
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
413
+ };
414
+
415
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
416
+
417
+ emitter.emit('formValuesChange', {
418
+ changedPaths: [['org_o2m']],
419
+ allValues: {
420
+ org_o2m: [{ __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } }],
421
+ },
422
+ });
423
+
424
+ expect(onChange).not.toHaveBeenCalled();
425
+ });
426
+
427
+ it('does not clear a row field when another row is removed before it and the row index changes', () => {
428
+ const emitter = new EventEmitter();
429
+ const formBlock = {
430
+ uid: 'form-1',
431
+ disposed: false,
432
+ emitter,
433
+ context: {
434
+ form: {},
435
+ formValues: {
436
+ org_o2m: [
437
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
438
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
439
+ ],
440
+ },
441
+ },
442
+ };
443
+
444
+ const onChange = vi.fn();
445
+ const model: any = {
446
+ disposed: false,
447
+ props: {
448
+ value: { id: 2 },
449
+ onChange,
450
+ },
451
+ context: {
452
+ blockModel: formBlock,
453
+ fieldIndex: ['org_o2m:1'],
454
+ },
455
+ };
456
+
457
+ const ctx: any = {
458
+ model,
459
+ flowKey: 'selectSettings',
460
+ };
461
+
462
+ const filter = {
463
+ logic: '$and',
464
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
465
+ };
466
+
467
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
468
+
469
+ emitter.emit('formValuesChange', {
470
+ changedPaths: [['org_o2m']],
471
+ allValues: {
472
+ org_o2m: [{ __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } }],
473
+ },
474
+ });
475
+
476
+ expect(onChange).not.toHaveBeenCalled();
477
+ });
478
+
479
+ it('does not clear a row field after reindexing when rows use a custom filter target key', () => {
480
+ const emitter = new EventEmitter();
481
+ const usersField = {
482
+ targetCollection: { filterTargetKey: 'name' },
483
+ isAssociationField: () => true,
484
+ };
485
+ const formBlock = {
486
+ uid: 'form-1',
487
+ disposed: false,
488
+ emitter,
489
+ context: {
490
+ form: {},
491
+ collection: { getField: (name: string) => (name === 'org_o2m' ? usersField : null) },
492
+ formValues: {
493
+ org_o2m: [
494
+ { name: 'alice', companyName: 'a', user_m2o: { id: 1 } },
495
+ { name: 'bob', companyName: 'b', user_m2o: { id: 2 } },
496
+ ],
497
+ },
498
+ },
499
+ };
500
+
501
+ const onChange = vi.fn();
502
+ const model: any = {
503
+ disposed: false,
504
+ props: {
505
+ value: { id: 2 },
506
+ onChange,
507
+ },
508
+ context: {
509
+ blockModel: formBlock,
510
+ fieldIndex: ['org_o2m:1'],
511
+ },
512
+ };
513
+
514
+ const ctx: any = {
515
+ model,
516
+ flowKey: 'selectSettings',
517
+ };
518
+
519
+ const filter = {
520
+ logic: '$and',
521
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
522
+ };
523
+
524
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
525
+
526
+ emitter.emit('formValuesChange', {
527
+ changedPaths: [['org_o2m']],
528
+ allValues: {
529
+ org_o2m: [{ name: 'bob', companyName: 'b', user_m2o: { id: 2 } }],
530
+ },
531
+ });
532
+
533
+ expect(onChange).not.toHaveBeenCalled();
534
+ });
535
+
536
+ it('does not clear a row field after reindexing when rows use a composite filter target key', () => {
537
+ const emitter = new EventEmitter();
538
+ const usersField = {
539
+ targetCollection: { filterTargetKey: ['tenantId', 'code'] },
540
+ isAssociationField: () => true,
541
+ };
542
+ const formBlock = {
543
+ uid: 'form-1',
544
+ disposed: false,
545
+ emitter,
546
+ context: {
547
+ form: {},
548
+ collection: { getField: (name: string) => (name === 'org_o2m' ? usersField : null) },
549
+ formValues: {
550
+ org_o2m: [
551
+ { tenantId: 1, code: 'a', companyName: 'a', user_m2o: { id: 1 } },
552
+ { tenantId: 1, code: 'b', companyName: 'b', user_m2o: { id: 2 } },
553
+ ],
554
+ },
555
+ },
556
+ };
557
+
558
+ const onChange = vi.fn();
559
+ const model: any = {
560
+ disposed: false,
561
+ props: {
562
+ value: { id: 2 },
563
+ onChange,
564
+ },
565
+ context: {
566
+ blockModel: formBlock,
567
+ fieldIndex: ['org_o2m:1'],
568
+ },
569
+ };
570
+
571
+ const ctx: any = {
572
+ model,
573
+ flowKey: 'selectSettings',
574
+ };
575
+
576
+ const filter = {
577
+ logic: '$and',
578
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
579
+ };
580
+
581
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
582
+
583
+ emitter.emit('formValuesChange', {
584
+ changedPaths: [['org_o2m']],
585
+ allValues: {
586
+ org_o2m: [{ tenantId: 1, code: 'b', companyName: 'b', user_m2o: { id: 2 } }],
587
+ },
588
+ });
589
+
590
+ expect(onChange).not.toHaveBeenCalled();
591
+ });
592
+
593
+ it('keeps tracking a row after its custom filter target key changes', () => {
594
+ const emitter = new EventEmitter();
595
+ const usersField = {
596
+ targetCollection: { filterTargetKey: 'name' },
597
+ isAssociationField: () => true,
598
+ };
599
+ const formBlock = {
600
+ uid: 'form-1',
601
+ disposed: false,
602
+ emitter,
603
+ context: {
604
+ form: {},
605
+ collection: { getField: (name: string) => (name === 'org_o2m' ? usersField : null) },
606
+ formValues: {
607
+ org_o2m: [
608
+ { name: 'alice', companyName: 'a', user_m2o: { id: 1 } },
609
+ { name: 'bob', companyName: 'b', user_m2o: { id: 2 } },
610
+ ],
611
+ },
612
+ },
613
+ };
614
+
615
+ const onChange = vi.fn();
616
+ const model: any = {
617
+ disposed: false,
618
+ props: {
619
+ value: { id: 2 },
620
+ onChange,
621
+ },
622
+ context: {
623
+ blockModel: formBlock,
624
+ fieldIndex: ['org_o2m:1'],
625
+ },
626
+ };
627
+
628
+ const ctx: any = {
629
+ model,
630
+ flowKey: 'selectSettings',
631
+ };
632
+
633
+ const filter = {
634
+ logic: '$and',
635
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
636
+ };
637
+
638
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
639
+
640
+ emitter.emit('formValuesChange', {
641
+ changedPaths: [['org_o2m', 1, 'name']],
642
+ allValues: {
643
+ org_o2m: [
644
+ { name: 'alice', companyName: 'a', user_m2o: { id: 1 } },
645
+ { name: 'robert', companyName: 'b', user_m2o: { id: 2 } },
646
+ ],
647
+ },
648
+ });
649
+
650
+ expect(onChange).not.toHaveBeenCalled();
651
+
652
+ emitter.emit('formValuesChange', {
653
+ changedPaths: [['org_o2m', 1, 'companyName']],
654
+ allValues: {
655
+ org_o2m: [
656
+ { name: 'alice', companyName: 'a', user_m2o: { id: 1 } },
657
+ { name: 'robert', companyName: 'c', user_m2o: { id: 2 } },
658
+ ],
659
+ },
660
+ });
661
+
662
+ expect(onChange).toHaveBeenCalledWith(null);
663
+ });
664
+
665
+ it('keeps tracking a row after its temporary identity changes to a persisted key', () => {
666
+ const emitter = new EventEmitter();
667
+ const usersField = {
668
+ targetCollection: { filterTargetKey: 'id' },
669
+ isAssociationField: () => true,
670
+ };
671
+ const formBlock = {
672
+ uid: 'form-1',
673
+ disposed: false,
674
+ emitter,
675
+ context: {
676
+ form: {},
677
+ collection: { getField: (name: string) => (name === 'org_o2m' ? usersField : null) },
678
+ formValues: {
679
+ org_o2m: [
680
+ { id: 1, companyName: 'a', user_m2o: { id: 1 } },
681
+ { __index__: 'row-b', __is_new__: true, companyName: 'b', user_m2o: { id: 2 } },
682
+ ],
683
+ },
684
+ },
685
+ };
686
+
687
+ const onChange = vi.fn();
688
+ const model: any = {
689
+ disposed: false,
690
+ props: {
691
+ value: { id: 2 },
692
+ onChange,
693
+ },
694
+ context: {
695
+ blockModel: formBlock,
696
+ fieldIndex: ['org_o2m:1'],
697
+ },
698
+ };
699
+
700
+ const ctx: any = {
701
+ model,
702
+ flowKey: 'selectSettings',
703
+ };
704
+
705
+ const filter = {
706
+ logic: '$and',
707
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
708
+ };
709
+
710
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
711
+
712
+ emitter.emit('formValuesChange', {
713
+ changedPaths: [['org_o2m', 1, 'id']],
714
+ allValues: {
715
+ org_o2m: [
716
+ { id: 1, companyName: 'a', user_m2o: { id: 1 } },
717
+ { __index__: 'row-b', id: 2, companyName: 'b', user_m2o: { id: 2 } },
718
+ ],
719
+ },
720
+ });
721
+
722
+ expect(onChange).not.toHaveBeenCalled();
723
+
724
+ emitter.emit('formValuesChange', {
725
+ changedPaths: [['org_o2m', 1, 'companyName']],
726
+ allValues: {
727
+ org_o2m: [
728
+ { id: 1, companyName: 'a', user_m2o: { id: 1 } },
729
+ { __index__: 'row-b', id: 2, companyName: 'c', user_m2o: { id: 2 } },
730
+ ],
731
+ },
732
+ });
733
+
734
+ expect(onChange).toHaveBeenCalledWith(null);
735
+ });
736
+
737
+ it('does not clear a stale row field binding when its row has been removed', () => {
738
+ const emitter = new EventEmitter();
739
+ const formBlock = {
740
+ uid: 'form-1',
741
+ disposed: false,
742
+ emitter,
743
+ context: {
744
+ form: {},
745
+ formValues: {
746
+ org_o2m: [
747
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
748
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
749
+ ],
750
+ },
751
+ },
752
+ };
753
+
754
+ const onChange = vi.fn();
755
+ const model: any = {
756
+ disposed: false,
757
+ props: {
758
+ value: { id: 2 },
759
+ onChange,
760
+ },
761
+ context: {
762
+ blockModel: formBlock,
763
+ fieldIndex: ['org_o2m:1'],
764
+ },
765
+ };
766
+
767
+ const ctx: any = {
768
+ model,
769
+ flowKey: 'selectSettings',
770
+ };
771
+
772
+ const filter = {
773
+ logic: '$and',
774
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
775
+ };
776
+
777
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
778
+
779
+ emitter.emit('formValuesChange', {
780
+ changedPaths: [['org_o2m']],
781
+ allValues: {
782
+ org_o2m: [{ __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } }],
783
+ },
784
+ });
785
+
786
+ expect(onChange).not.toHaveBeenCalled();
787
+ });
788
+
789
+ it('does not reattach a stale row field binding to the next row after deletion', () => {
790
+ const emitter = new EventEmitter();
791
+ const formBlock = {
792
+ uid: 'form-1',
793
+ disposed: false,
794
+ emitter,
795
+ context: {
796
+ form: {},
797
+ formValues: {
798
+ org_o2m: [
799
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
800
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
801
+ { __index__: 'row-c', companyName: 'c', user_m2o: { id: 3 } },
802
+ ],
803
+ },
804
+ },
805
+ };
806
+
807
+ const onChange = vi.fn();
808
+ const model: any = {
809
+ disposed: false,
810
+ props: {
811
+ value: { id: 2 },
812
+ onChange,
813
+ },
814
+ context: {
815
+ blockModel: formBlock,
816
+ fieldIndex: ['org_o2m:1'],
817
+ },
818
+ };
819
+
820
+ const ctx: any = {
821
+ model,
822
+ flowKey: 'selectSettings',
823
+ };
824
+
825
+ const filter = {
826
+ logic: '$and',
827
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
828
+ };
829
+
830
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
831
+
832
+ emitter.emit('formValuesChange', {
833
+ changedPaths: [['org_o2m']],
834
+ allValues: {
835
+ org_o2m: [
836
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
837
+ { __index__: 'row-c', companyName: 'c', user_m2o: { id: 3 } },
838
+ ],
839
+ },
840
+ });
841
+
842
+ emitter.emit('formValuesChange', {
843
+ changedPaths: [['org_o2m', 1, 'companyName']],
844
+ allValues: {
845
+ org_o2m: [
846
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
847
+ { __index__: 'row-c', companyName: 'changed', user_m2o: { id: 3 } },
848
+ ],
849
+ },
850
+ });
851
+
852
+ expect(onChange).not.toHaveBeenCalled();
853
+ });
854
+
855
+ it('does not reattach a stale row field binding when the first stale event is an old index child change', () => {
856
+ const emitter = new EventEmitter();
857
+ const formBlock = {
858
+ uid: 'form-1',
859
+ disposed: false,
860
+ emitter,
861
+ context: {
862
+ form: {},
863
+ formValues: {
864
+ org_o2m: [
865
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
866
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
867
+ { __index__: 'row-c', companyName: 'c', user_m2o: { id: 3 } },
868
+ ],
869
+ },
870
+ },
871
+ };
872
+
873
+ const onChange = vi.fn();
874
+ const model: any = {
875
+ disposed: false,
876
+ props: {
877
+ value: { id: 2 },
878
+ onChange,
879
+ },
880
+ context: {
881
+ blockModel: formBlock,
882
+ fieldIndex: ['org_o2m:1'],
883
+ },
884
+ };
885
+
886
+ const ctx: any = {
887
+ model,
888
+ flowKey: 'selectSettings',
889
+ };
890
+
891
+ const filter = {
892
+ logic: '$and',
893
+ items: [{ path: 'name', operator: '$includes', value: '{{ ctx.item.value.companyName }}' }],
894
+ };
895
+
896
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
897
+
898
+ emitter.emit('formValuesChange', {
899
+ changedPaths: [['org_o2m', 1, 'companyName']],
900
+ allValues: {
901
+ org_o2m: [
902
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
903
+ { __index__: 'row-c', companyName: 'changed', user_m2o: { id: 3 } },
904
+ ],
905
+ },
906
+ });
907
+
908
+ expect(onChange).not.toHaveBeenCalled();
909
+ });
910
+
911
+ it('does not clear a stale removed row binding when data scope depends on the item length', () => {
912
+ const emitter = new EventEmitter();
913
+ const formBlock = {
914
+ uid: 'form-1',
915
+ disposed: false,
916
+ emitter,
917
+ context: {
918
+ form: {},
919
+ formValues: {
920
+ org_o2m: [
921
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
922
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
923
+ ],
924
+ },
925
+ },
926
+ };
927
+
928
+ const onChange = vi.fn();
929
+ const model: any = {
930
+ disposed: false,
931
+ props: {
932
+ value: { id: 2 },
933
+ onChange,
934
+ },
935
+ context: {
936
+ blockModel: formBlock,
937
+ fieldIndex: ['org_o2m:1'],
938
+ },
939
+ };
940
+
941
+ const ctx: any = {
942
+ model,
943
+ flowKey: 'selectSettings',
944
+ };
945
+
946
+ const filter = {
947
+ logic: '$and',
948
+ items: [{ path: 'count', operator: '$eq', value: '{{ ctx.item.length }}' }],
949
+ };
950
+
951
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
952
+
953
+ emitter.emit('formValuesChange', {
954
+ changedPaths: [['org_o2m']],
955
+ allValues: {
956
+ org_o2m: [{ __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } }],
957
+ },
958
+ });
959
+
960
+ expect(onChange).not.toHaveBeenCalled();
961
+ });
962
+
963
+ it('still clears a row field when data scope explicitly depends on the item length', () => {
964
+ const emitter = new EventEmitter();
965
+ const formBlock = {
966
+ uid: 'form-1',
967
+ disposed: false,
968
+ emitter,
969
+ context: {
970
+ form: {},
971
+ formValues: {
972
+ org_o2m: [
973
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
974
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
975
+ ],
976
+ },
977
+ },
978
+ };
979
+
980
+ const onChange = vi.fn();
981
+ const model: any = {
982
+ disposed: false,
983
+ props: {
984
+ value: { id: 1 },
985
+ onChange,
986
+ },
987
+ context: {
988
+ blockModel: formBlock,
989
+ fieldIndex: ['org_o2m:0'],
990
+ },
991
+ };
992
+
993
+ const ctx: any = {
994
+ model,
995
+ flowKey: 'selectSettings',
996
+ };
997
+
998
+ const filter = {
999
+ logic: '$and',
1000
+ items: [{ path: 'count', operator: '$eq', value: '{{ ctx.item.length }}' }],
1001
+ };
1002
+
1003
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
1004
+
1005
+ emitter.emit('formValuesChange', {
1006
+ changedPaths: [['org_o2m']],
1007
+ allValues: {
1008
+ org_o2m: [{ __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } }],
1009
+ },
1010
+ });
1011
+
1012
+ expect(onChange).toHaveBeenCalledWith(null);
1013
+ });
1014
+
1015
+ it('does not clear a row field when data scope depends on the item index and another row is removed after it', () => {
1016
+ const emitter = new EventEmitter();
1017
+ const formBlock = {
1018
+ uid: 'form-1',
1019
+ disposed: false,
1020
+ emitter,
1021
+ context: {
1022
+ form: {},
1023
+ formValues: {
1024
+ org_o2m: [
1025
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
1026
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
1027
+ ],
1028
+ },
1029
+ },
1030
+ };
1031
+
1032
+ const onChange = vi.fn();
1033
+ const model: any = {
1034
+ disposed: false,
1035
+ props: {
1036
+ value: { id: 1 },
1037
+ onChange,
1038
+ },
1039
+ context: {
1040
+ blockModel: formBlock,
1041
+ fieldIndex: ['org_o2m:0'],
1042
+ },
1043
+ };
1044
+
1045
+ const ctx: any = {
1046
+ model,
1047
+ flowKey: 'selectSettings',
1048
+ };
1049
+
1050
+ const filter = {
1051
+ logic: '$and',
1052
+ items: [{ path: 'index', operator: '$eq', value: '{{ ctx.item.index }}' }],
1053
+ };
1054
+
1055
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
1056
+
1057
+ emitter.emit('formValuesChange', {
1058
+ changedPaths: [['org_o2m']],
1059
+ allValues: {
1060
+ org_o2m: [{ __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } }],
1061
+ },
1062
+ });
1063
+
1064
+ expect(onChange).not.toHaveBeenCalled();
1065
+ });
1066
+
1067
+ it('clears a row field when data scope depends on the item index and the row index changes', () => {
1068
+ const emitter = new EventEmitter();
1069
+ const formBlock = {
1070
+ uid: 'form-1',
1071
+ disposed: false,
1072
+ emitter,
1073
+ context: {
1074
+ form: {},
1075
+ formValues: {
1076
+ org_o2m: [
1077
+ { __index__: 'row-a', companyName: 'a', user_m2o: { id: 1 } },
1078
+ { __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } },
1079
+ ],
1080
+ },
1081
+ },
1082
+ };
1083
+
1084
+ const onChange = vi.fn();
1085
+ const model: any = {
1086
+ disposed: false,
1087
+ props: {
1088
+ value: { id: 2 },
1089
+ onChange,
1090
+ },
1091
+ context: {
1092
+ blockModel: formBlock,
1093
+ fieldIndex: ['org_o2m:1'],
1094
+ },
1095
+ };
1096
+
1097
+ const ctx: any = {
1098
+ model,
1099
+ flowKey: 'selectSettings',
1100
+ };
1101
+
1102
+ const filter = {
1103
+ logic: '$and',
1104
+ items: [{ path: 'index', operator: '$eq', value: '{{ ctx.item.index }}' }],
1105
+ };
1106
+
1107
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
1108
+
1109
+ emitter.emit('formValuesChange', {
1110
+ changedPaths: [['org_o2m']],
1111
+ allValues: {
1112
+ org_o2m: [{ __index__: 'row-b', companyName: 'b', user_m2o: { id: 2 } }],
1113
+ },
1114
+ });
1115
+
1116
+ expect(onChange).toHaveBeenCalledWith(null);
1117
+ });
96
1118
  });