@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
@@ -535,4 +535,482 @@ describe('fieldLinkageRules action - linkage scope metadata', () => {
535
535
  linkageScopeDepth: 0,
536
536
  });
537
537
  });
538
+
539
+ it('runs row-scoped linkage rules for subtable row forks without subModels.items and deduplicates the same row', async () => {
540
+ const setFormValues = vi.fn(async () => undefined);
541
+ const linkageAssignHandler = vi.fn((actionCtx: any, { value, addFormValuePatch }: any) => {
542
+ expect(actionCtx.linkageScopeDepth).toBe(0);
543
+ expect(actionCtx.item?.value?.uid).toBe('role-uid-1');
544
+ expect(value[0]?.value).toBe('role-uid-1');
545
+ addFormValuePatch({ path: 'roles.name', value: value[0].value });
546
+ });
547
+
548
+ const engine = new FlowEngine();
549
+ const makeRowFork = (uid: string) => {
550
+ const rowFork = new FlowModel({ uid, flowEngine: engine }) as any;
551
+ rowFork.context.defineMethod('subTableRowFork', () => true);
552
+ rowFork.context.defineProperty('fieldIndex', {
553
+ value: ['roles:0'],
554
+ });
555
+ rowFork.context.defineProperty('item', {
556
+ value: {
557
+ index: 0,
558
+ __is_new__: true,
559
+ value: {
560
+ uid: 'role-uid-1',
561
+ },
562
+ },
563
+ });
564
+ rowFork.context.defineProperty('setFormValues', {
565
+ value: setFormValues,
566
+ });
567
+ rowFork.context.defineProperty('app', {
568
+ value: {
569
+ jsonLogic: {
570
+ apply: () => true,
571
+ },
572
+ },
573
+ });
574
+ rowFork.getAction = vi.fn((name: string) => {
575
+ if (name === 'linkageAssignField') {
576
+ return {
577
+ handler: linkageAssignHandler,
578
+ };
579
+ }
580
+ });
581
+ return rowFork;
582
+ };
583
+
584
+ const rowFork1 = makeRowFork('role-row-fork-1');
585
+ const rowFork2 = makeRowFork('role-row-fork-2');
586
+
587
+ const masterModel1: any = {
588
+ uid: 'master-role-grid-1',
589
+ forks: new Set([rowFork1]),
590
+ };
591
+ const masterModel2: any = {
592
+ uid: 'master-role-grid-2',
593
+ forks: new Set([rowFork2]),
594
+ };
595
+
596
+ const rolesField: any = {
597
+ type: 'hasMany',
598
+ isAssociationField: () => true,
599
+ targetCollection: {
600
+ getField: (name: string) => ({ name, isAssociationField: () => false }),
601
+ },
602
+ };
603
+
604
+ const gridModel: any = {
605
+ uid: 'grid-model-role-row',
606
+ context: {
607
+ blockModel: {
608
+ collection: {
609
+ getField: (name: string) => (name === 'roles' ? rolesField : null),
610
+ },
611
+ },
612
+ },
613
+ getAction: vi.fn((name: string) => {
614
+ if (name === 'linkageAssignField') {
615
+ return {
616
+ handler: linkageAssignHandler,
617
+ };
618
+ }
619
+ }),
620
+ __allModels: [],
621
+ };
622
+
623
+ const ctx: any = {
624
+ model: gridModel,
625
+ engine: {
626
+ forEachModel: (cb: (m: any) => void) => {
627
+ cb(masterModel1);
628
+ cb(masterModel2);
629
+ },
630
+ },
631
+ flowKey: 'eventSettings',
632
+ inputArgs: {
633
+ source: 'user',
634
+ txId: 'tx-role-row',
635
+ changedPaths: [['roles', 0, 'uid']],
636
+ },
637
+ app: {
638
+ jsonLogic: {
639
+ apply: () => true,
640
+ },
641
+ },
642
+ resolveJsonTemplate: async (v: any) => {
643
+ if (v === '{{ ctx.item.value.uid }}') {
644
+ return 'role-uid-1';
645
+ }
646
+ return v;
647
+ },
648
+ };
649
+
650
+ await fieldLinkageRules.handler(ctx, {
651
+ value: [
652
+ {
653
+ key: 'rule-role-row',
654
+ title: 'rule-role-row',
655
+ enable: true,
656
+ condition: { logic: '$and', items: [] },
657
+ actions: [
658
+ {
659
+ name: 'linkageAssignField',
660
+ params: {
661
+ value: [
662
+ {
663
+ key: 'r1',
664
+ enable: true,
665
+ targetPath: 'roles.name',
666
+ mode: 'assign',
667
+ value: '{{ ctx.item.value.uid }}',
668
+ condition: { logic: '$and', items: [] },
669
+ },
670
+ ],
671
+ },
672
+ },
673
+ ],
674
+ },
675
+ ],
676
+ });
677
+
678
+ expect(linkageAssignHandler).toHaveBeenCalledTimes(1);
679
+ expect(setFormValues).toHaveBeenCalledTimes(1);
680
+ expect(setFormValues).toHaveBeenCalledWith(
681
+ [
682
+ {
683
+ path: ['roles', 0, 'name'],
684
+ value: 'role-uid-1',
685
+ },
686
+ ],
687
+ expect.objectContaining({
688
+ source: 'linkage',
689
+ linkageTxId: 'tx-role-row',
690
+ linkageScopeDepth: 0,
691
+ }),
692
+ );
693
+ });
694
+
695
+ it('keeps row-scoped default patches following while current value is still the last default', async () => {
696
+ const store = {
697
+ roles: [{ uid: 'role-uid-1', name: '' }],
698
+ };
699
+ const getAt = (path: Array<string | number>) => path.reduce((acc: any, seg) => acc?.[seg], store as any);
700
+ const setAt = (path: Array<string | number>, value: any) => {
701
+ let cur: any = store;
702
+ for (const seg of path.slice(0, -1)) {
703
+ cur = cur[seg];
704
+ }
705
+ cur[path[path.length - 1]] = value;
706
+ };
707
+ const pathKey = (path: Array<string | number>) => JSON.stringify(path);
708
+ const lastDefaults = new Map<string, any>();
709
+ const form = {
710
+ getFieldValue: (path: Array<string | number>) => getAt(path),
711
+ };
712
+ const formValueRuntime = {
713
+ canApplyDefaultValuePatch: vi.fn((path: Array<string | number>, value: any) => {
714
+ const current = getAt(path);
715
+ const last = lastDefaults.get(pathKey(path));
716
+ if (current === undefined || current === null || current === '') return true;
717
+ if (typeof last !== 'undefined' && current === last) return true;
718
+ if (current === value) {
719
+ lastDefaults.set(pathKey(path), value);
720
+ }
721
+ return false;
722
+ }),
723
+ recordDefaultValuePatch: vi.fn((path: Array<string | number>, value: any) => {
724
+ lastDefaults.set(pathKey(path), value);
725
+ }),
726
+ };
727
+ const setFormValues = vi.fn(async (patches: Array<{ path: Array<string | number>; value: any }>) => {
728
+ for (const patch of patches) {
729
+ setAt(patch.path, patch.value);
730
+ }
731
+ });
732
+ const defaultHandler = vi.fn((actionCtx: any, { value, addFormValuePatch }: any) => {
733
+ addFormValuePatch({ path: value[0].targetPath, value: value[0].value, whenEmpty: true });
734
+ });
735
+
736
+ const engine = new FlowEngine();
737
+ const rowFork = new FlowModel({ uid: 'role-default-row-fork', flowEngine: engine }) as any;
738
+ rowFork.context.defineProperty('subTableRowFork', {
739
+ value: true,
740
+ });
741
+ rowFork.context.defineProperty('fieldIndex', {
742
+ value: ['roles:0'],
743
+ });
744
+ rowFork.context.defineProperty('form', {
745
+ value: form,
746
+ });
747
+ rowFork.context.defineProperty('item', {
748
+ get: () => ({
749
+ index: 0,
750
+ __is_new__: true,
751
+ value: store.roles[0],
752
+ }),
753
+ });
754
+ rowFork.context.defineProperty('setFormValues', {
755
+ value: setFormValues,
756
+ });
757
+ rowFork.context.defineProperty('app', {
758
+ value: {
759
+ jsonLogic: {
760
+ apply: () => true,
761
+ },
762
+ },
763
+ });
764
+ rowFork.getAction = vi.fn((name: string) => {
765
+ if (name === 'setFieldsDefaultValue') {
766
+ return {
767
+ handler: defaultHandler,
768
+ };
769
+ }
770
+ });
771
+
772
+ const masterModel: any = {
773
+ uid: 'master-role-default-grid',
774
+ forks: new Set([rowFork]),
775
+ };
776
+ const rolesField: any = {
777
+ type: 'hasMany',
778
+ isAssociationField: () => true,
779
+ targetCollection: {
780
+ getField: (name: string) => ({ name, isAssociationField: () => false }),
781
+ },
782
+ };
783
+ const blockModel: any = {
784
+ collection: {
785
+ getField: (name: string) => (name === 'roles' ? rolesField : null),
786
+ },
787
+ formValueRuntime,
788
+ };
789
+ rowFork.context.defineProperty('blockModel', {
790
+ value: blockModel,
791
+ });
792
+ const gridModel: any = {
793
+ uid: 'grid-model-role-default-row',
794
+ context: {
795
+ blockModel,
796
+ },
797
+ getAction: vi.fn((name: string) => {
798
+ if (name === 'setFieldsDefaultValue') {
799
+ return {
800
+ handler: defaultHandler,
801
+ };
802
+ }
803
+ }),
804
+ __allModels: [],
805
+ };
806
+ const ctx: any = {
807
+ model: gridModel,
808
+ engine: {
809
+ forEachModel: (cb: (m: any) => void) => {
810
+ cb(masterModel);
811
+ },
812
+ },
813
+ flowKey: 'eventSettings',
814
+ inputArgs: {
815
+ source: 'user',
816
+ txId: 'tx-role-default-row',
817
+ changedPaths: [['roles', 0, 'uid']],
818
+ },
819
+ app: {
820
+ jsonLogic: {
821
+ apply: () => true,
822
+ },
823
+ },
824
+ resolveJsonTemplate: async (v: any) => v,
825
+ };
826
+ const makeParams = (value: string) => ({
827
+ value: [
828
+ {
829
+ key: `rule-${value}`,
830
+ title: `rule-${value}`,
831
+ enable: true,
832
+ condition: { logic: '$and', items: [] },
833
+ actions: [
834
+ {
835
+ name: 'setFieldsDefaultValue',
836
+ params: {
837
+ value: [
838
+ {
839
+ key: `r-${value}`,
840
+ enable: true,
841
+ targetPath: 'roles.name',
842
+ mode: 'default',
843
+ value,
844
+ condition: { logic: '$and', items: [] },
845
+ },
846
+ ],
847
+ },
848
+ },
849
+ ],
850
+ },
851
+ ],
852
+ });
853
+
854
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-1'));
855
+ expect(store.roles[0].name).toBe('role-uid-1');
856
+
857
+ store.roles[0].uid = 'role-uid-2';
858
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-2'));
859
+ expect(store.roles[0].name).toBe('role-uid-2');
860
+ expect(setFormValues).toHaveBeenCalledTimes(2);
861
+ expect(formValueRuntime.canApplyDefaultValuePatch).toHaveBeenCalledTimes(2);
862
+ expect(formValueRuntime.recordDefaultValuePatch).toHaveBeenCalledTimes(2);
863
+
864
+ store.roles[0].name = 'manual';
865
+ store.roles[0].uid = 'role-uid-3';
866
+ await fieldLinkageRules.handler(ctx, makeParams('role-uid-3'));
867
+ expect(store.roles[0].name).toBe('manual');
868
+ expect(setFormValues).toHaveBeenCalledTimes(2);
869
+ });
870
+
871
+ it('skips stale subtable row forks after the row has been removed', async () => {
872
+ const setFormValues = vi.fn(async () => undefined);
873
+ const defaultHandler = vi.fn((actionCtx: any, { addFormValuePatch }: any) => {
874
+ addFormValuePatch({ path: 'roles.title', value: 'stale-title', whenEmpty: true });
875
+ });
876
+ const form = {
877
+ getFieldValue: vi.fn((path: Array<string | number>) => {
878
+ if (JSON.stringify(path) === JSON.stringify(['roles', 2])) {
879
+ return undefined;
880
+ }
881
+ if (JSON.stringify(path) === JSON.stringify(['roles'])) {
882
+ return [
883
+ { name: '1', title: '1' },
884
+ { name: '3', title: '3' },
885
+ ];
886
+ }
887
+ }),
888
+ };
889
+
890
+ const engine = new FlowEngine();
891
+ const staleRowFork = new FlowModel({ uid: 'stale-role-row-fork', flowEngine: engine }) as any;
892
+ staleRowFork.context.defineProperty('subTableRowFork', {
893
+ value: true,
894
+ });
895
+ staleRowFork.context.defineProperty('fieldIndex', {
896
+ value: ['roles:2'],
897
+ });
898
+ staleRowFork.context.defineProperty('form', {
899
+ value: form,
900
+ });
901
+ staleRowFork.context.defineProperty('item', {
902
+ value: {
903
+ index: 2,
904
+ __is_new__: true,
905
+ value: { name: '3', title: '3' },
906
+ },
907
+ });
908
+ staleRowFork.context.defineProperty('setFormValues', {
909
+ value: setFormValues,
910
+ });
911
+ staleRowFork.context.defineProperty('app', {
912
+ value: {
913
+ jsonLogic: {
914
+ apply: () => true,
915
+ },
916
+ },
917
+ });
918
+ staleRowFork.getAction = vi.fn((name: string) => {
919
+ if (name === 'setFieldsDefaultValue') {
920
+ return {
921
+ handler: defaultHandler,
922
+ };
923
+ }
924
+ });
925
+
926
+ const masterModel: any = {
927
+ uid: 'master-stale-role-grid',
928
+ forks: new Set([staleRowFork]),
929
+ };
930
+ const rolesField: any = {
931
+ type: 'hasMany',
932
+ isAssociationField: () => true,
933
+ targetCollection: {
934
+ getField: (name: string) => ({ name, isAssociationField: () => false }),
935
+ },
936
+ };
937
+ const blockModel: any = {
938
+ collection: {
939
+ getField: (name: string) => (name === 'roles' ? rolesField : null),
940
+ },
941
+ formValueRuntime: {
942
+ canApplyDefaultValuePatch: vi.fn(() => true),
943
+ recordDefaultValuePatch: vi.fn(),
944
+ },
945
+ };
946
+ staleRowFork.context.defineProperty('blockModel', {
947
+ value: blockModel,
948
+ });
949
+
950
+ const gridModel: any = {
951
+ uid: 'grid-model-stale-role-row',
952
+ context: {
953
+ blockModel,
954
+ },
955
+ getAction: vi.fn((name: string) => {
956
+ if (name === 'setFieldsDefaultValue') {
957
+ return {
958
+ handler: defaultHandler,
959
+ };
960
+ }
961
+ }),
962
+ __allModels: [],
963
+ };
964
+ const ctx: any = {
965
+ model: gridModel,
966
+ engine: {
967
+ forEachModel: (cb: (m: any) => void) => {
968
+ cb(masterModel);
969
+ },
970
+ },
971
+ flowKey: 'eventSettings',
972
+ inputArgs: {
973
+ source: 'user',
974
+ txId: 'tx-stale-row',
975
+ changedPaths: [['roles']],
976
+ },
977
+ app: {
978
+ jsonLogic: {
979
+ apply: () => true,
980
+ },
981
+ },
982
+ resolveJsonTemplate: async (v: any) => v,
983
+ };
984
+
985
+ await fieldLinkageRules.handler(ctx, {
986
+ value: [
987
+ {
988
+ key: 'rule-stale-row',
989
+ title: 'rule-stale-row',
990
+ enable: true,
991
+ condition: { logic: '$and', items: [] },
992
+ actions: [
993
+ {
994
+ name: 'setFieldsDefaultValue',
995
+ params: {
996
+ value: [
997
+ {
998
+ key: 'r-stale-row',
999
+ enable: true,
1000
+ targetPath: 'roles.title',
1001
+ mode: 'default',
1002
+ value: 'stale-title',
1003
+ condition: { logic: '$and', items: [] },
1004
+ },
1005
+ ],
1006
+ },
1007
+ },
1008
+ ],
1009
+ },
1010
+ ],
1011
+ });
1012
+
1013
+ expect(defaultHandler).not.toHaveBeenCalled();
1014
+ expect(setFormValues).not.toHaveBeenCalled();
1015
+ });
538
1016
  });
@@ -0,0 +1,190 @@
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
+ import { describe, expect, it, vi } from 'vitest';
11
+ import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
12
+ import { FieldModel } from '../../models/base/FieldModel';
13
+ import { JSEditableFieldModel } from '../../models/fields/JSEditableFieldModel';
14
+ import { DetailsItemModel } from '../../models/blocks/details/DetailsItemModel';
15
+ import { pattern } from '../pattern';
16
+
17
+ class DummyDisplayFieldModel extends FieldModel {}
18
+
19
+ class DummyFormItemModel extends FlowModel<{ subModels: { field?: FieldModel } }> {
20
+ collectionField: any;
21
+
22
+ static getDefaultBindingByField() {
23
+ return { modelName: 'FieldModel' };
24
+ }
25
+
26
+ getFieldSettingsInitParams() {
27
+ return { mock: true };
28
+ }
29
+ }
30
+
31
+ function makeCollectionField() {
32
+ return {
33
+ targetCollection: undefined,
34
+ isAssociationField: () => false,
35
+ };
36
+ }
37
+
38
+ function makeCtx(parent: DummyFormItemModel) {
39
+ const collectionField = makeCollectionField();
40
+ parent.collectionField = collectionField;
41
+ return {
42
+ model: parent,
43
+ collectionField,
44
+ engine: parent.flowEngine,
45
+ } as any;
46
+ }
47
+
48
+ describe('pattern action', () => {
49
+ it('keeps JS editable field model when switching to display only', async () => {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({
52
+ DummyFormItemModel,
53
+ FieldModel,
54
+ JSEditableFieldModel,
55
+ DummyDisplayFieldModel,
56
+ });
57
+
58
+ const parent = engine.createModel<DummyFormItemModel>({
59
+ use: DummyFormItemModel,
60
+ uid: 'form-item-js',
61
+ subModels: {
62
+ field: {
63
+ use: FieldModel,
64
+ uid: 'field-js',
65
+ stepParams: {
66
+ fieldBinding: {
67
+ use: 'JSEditableFieldModel',
68
+ },
69
+ jsSettings: {
70
+ runJs: {
71
+ code: 'ctx.render("hello")',
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ });
78
+ const field = parent.subModels.field;
79
+ const saveSpy = vi.spyOn(engine, 'saveModel');
80
+ const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
81
+
82
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'editable' });
83
+
84
+ expect(parent.subModels.field).toBe(field);
85
+ expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
86
+ expect(parent.subModels.field?.uid).toBe('field-js');
87
+ expect(parent.subModels.field?.getStepParams('jsSettings', 'runJs')).toMatchObject({
88
+ code: 'ctx.render("hello")',
89
+ });
90
+ expect(saveSpy).not.toHaveBeenCalled();
91
+ expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
92
+ });
93
+
94
+ it('keeps JS editable field model when leaving display only', async () => {
95
+ const engine = new FlowEngine();
96
+ engine.registerModels({
97
+ DummyFormItemModel,
98
+ FieldModel,
99
+ JSEditableFieldModel,
100
+ DummyDisplayFieldModel,
101
+ });
102
+
103
+ const parent = engine.createModel<DummyFormItemModel>({
104
+ use: DummyFormItemModel,
105
+ uid: 'form-item-js-leave',
106
+ subModels: {
107
+ field: {
108
+ use: FieldModel,
109
+ uid: 'field-js-leave',
110
+ stepParams: {
111
+ fieldBinding: {
112
+ use: 'JSEditableFieldModel',
113
+ },
114
+ },
115
+ },
116
+ },
117
+ });
118
+ const field = parent.subModels.field;
119
+ const saveSpy = vi.spyOn(engine, 'saveModel');
120
+ const applyJsSettingsSpy = vi.spyOn(field as JSEditableFieldModel, 'scheduleApplyJsSettings');
121
+
122
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'editable' }, { pattern: 'readPretty' });
123
+
124
+ expect(parent.subModels.field).toBe(field);
125
+ expect(parent.subModels.field).toBeInstanceOf(JSEditableFieldModel);
126
+ expect(saveSpy).not.toHaveBeenCalled();
127
+ expect(applyJsSettingsSpy).toHaveBeenCalledTimes(1);
128
+ });
129
+
130
+ it('does not reapply JS settings when pattern is unchanged', async () => {
131
+ const engine = new FlowEngine();
132
+ engine.registerModels({
133
+ DummyFormItemModel,
134
+ FieldModel,
135
+ JSEditableFieldModel,
136
+ });
137
+
138
+ const parent = engine.createModel<DummyFormItemModel>({
139
+ use: DummyFormItemModel,
140
+ uid: 'form-item-js-unchanged',
141
+ subModels: {
142
+ field: {
143
+ use: FieldModel,
144
+ uid: 'field-js-unchanged',
145
+ stepParams: {
146
+ fieldBinding: {
147
+ use: 'JSEditableFieldModel',
148
+ },
149
+ },
150
+ },
151
+ },
152
+ });
153
+ const applyJsSettingsSpy = vi.spyOn(parent.subModels.field as JSEditableFieldModel, 'scheduleApplyJsSettings');
154
+
155
+ await pattern.afterParamsSave?.(makeCtx(parent), { pattern: 'readPretty' }, { pattern: 'readPretty' });
156
+
157
+ expect(applyJsSettingsSpy).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it('still rebuilds regular fields when switching to display only', async () => {
161
+ const engine = new FlowEngine();
162
+ engine.registerModels({
163
+ DummyFormItemModel,
164
+ FieldModel,
165
+ DummyDisplayFieldModel,
166
+ });
167
+
168
+ const parent = engine.createModel<DummyFormItemModel>({
169
+ use: DummyFormItemModel,
170
+ uid: 'form-item-regular',
171
+ subModels: {
172
+ field: {
173
+ use: FieldModel,
174
+ uid: 'field-regular',
175
+ },
176
+ },
177
+ });
178
+ const ctx = makeCtx(parent);
179
+ const displayBinding = { modelName: 'DummyDisplayFieldModel', defaultProps: { display: true } } as any;
180
+ const getDisplayBindingSpy = vi.spyOn(DetailsItemModel, 'getDefaultBindingByField').mockReturnValue(displayBinding);
181
+
182
+ await pattern.afterParamsSave?.(ctx, { pattern: 'readPretty' }, { pattern: 'editable' });
183
+
184
+ expect(parent.subModels.field).toBeInstanceOf(DummyDisplayFieldModel);
185
+ expect(parent.subModels.field?.uid).toBe('field-regular');
186
+ expect(parent.subModels.field?.props).toMatchObject({ display: true });
187
+
188
+ getDisplayBindingSpy.mockRestore();
189
+ });
190
+ });