@vendure/dashboard 3.6.0-minor-202511061550 → 3.6.0-minor-202512161252

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 (152) hide show
  1. package/dist/plugin/constants.js +2 -2
  2. package/dist/vite/constants.js +1 -0
  3. package/dist/vite/utils/compiler.d.ts +1 -0
  4. package/dist/vite/utils/compiler.js +5 -4
  5. package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
  6. package/dist/vite/utils/get-dashboard-paths.js +20 -0
  7. package/dist/vite/vite-plugin-dashboard-metadata.js +2 -1
  8. package/dist/vite/vite-plugin-tailwind-source.js +2 -15
  9. package/dist/vite/vite-plugin-translations.d.ts +10 -1
  10. package/dist/vite/vite-plugin-translations.js +156 -45
  11. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
  12. package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
  13. package/lingui.config.js +1 -0
  14. package/package.json +7 -7
  15. package/src/app/routeTree.gen.ts +1221 -0
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +9 -12
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +9 -12
  18. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +6 -9
  19. package/src/app/routes/_authenticated/_channels/channels.tsx +9 -12
  20. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +9 -12
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +9 -12
  22. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +9 -12
  23. package/src/app/routes/_authenticated/_countries/countries.tsx +9 -12
  24. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +9 -12
  25. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +9 -12
  26. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +9 -12
  27. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +0 -1
  28. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -12
  29. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +9 -12
  30. package/src/app/routes/_authenticated/_facets/facets.tsx +9 -12
  31. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +9 -12
  32. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +9 -12
  33. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +10 -13
  34. package/src/app/routes/_authenticated/_orders/components/add-surcharge-form.tsx +139 -0
  35. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +3 -0
  36. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +3 -1
  37. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +3 -3
  38. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +41 -41
  39. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  40. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +49 -11
  41. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +4 -1
  42. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +2 -3
  43. package/src/app/routes/_authenticated/_orders/orders.tsx +3 -3
  44. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +12 -3
  45. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +27 -30
  46. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +23 -0
  47. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +9 -12
  48. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +9 -12
  49. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +3 -3
  50. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +2 -2
  51. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +1 -0
  52. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +10 -12
  53. package/src/app/routes/_authenticated/_products/products.graphql.ts +1 -0
  54. package/src/app/routes/_authenticated/_products/products.tsx +15 -18
  55. package/src/app/routes/_authenticated/_products/products_.$id.tsx +9 -12
  56. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +9 -12
  57. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +9 -12
  58. package/src/app/routes/_authenticated/_profile/profile.tsx +3 -3
  59. package/src/app/routes/_authenticated/_promotions/promotions.tsx +9 -12
  60. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +9 -12
  61. package/src/app/routes/_authenticated/_roles/roles.tsx +9 -12
  62. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +9 -12
  63. package/src/app/routes/_authenticated/_sellers/sellers.tsx +9 -12
  64. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +9 -12
  65. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +11 -12
  66. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +19 -20
  67. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +9 -12
  68. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +9 -12
  69. package/src/app/routes/_authenticated/_system/healthchecks.tsx +2 -3
  70. package/src/app/routes/_authenticated/_system/job-queue.tsx +3 -3
  71. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +9 -12
  72. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +9 -12
  73. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -12
  74. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +9 -12
  75. package/src/app/routes/_authenticated/_zones/components/zone-bulk-actions.tsx +49 -1
  76. package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +34 -16
  77. package/src/app/routes/_authenticated/_zones/zones.tsx +9 -12
  78. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +9 -12
  79. package/src/app/routes/_authenticated/index.tsx +5 -3
  80. package/src/i18n/locales/bg.po +3436 -0
  81. package/src/lib/components/data-input/datetime-input.tsx +1 -1
  82. package/src/lib/components/data-input/default-relation-input.tsx +1 -1
  83. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  84. package/src/lib/components/data-input/string-list-input.tsx +188 -26
  85. package/src/lib/components/data-input/struct-form-input.tsx +175 -174
  86. package/src/lib/components/data-table/column-header-wrapper.tsx +1 -1
  87. package/src/lib/components/data-table/data-table-filter-badge.tsx +2 -2
  88. package/src/lib/components/data-table/data-table.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +1 -1
  90. package/src/lib/components/layout/channel-switcher.tsx +6 -2
  91. package/src/lib/components/layout/content-language-selector.tsx +6 -7
  92. package/src/lib/components/layout/dev-mode-indicator.tsx +7 -3
  93. package/src/lib/components/layout/language-dialog.tsx +26 -13
  94. package/src/lib/components/layout/manage-languages-dialog.tsx +10 -29
  95. package/src/lib/components/layout/nav-item-wrapper.tsx +1 -1
  96. package/src/lib/components/shared/asset/asset-gallery.tsx +8 -3
  97. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +14 -16
  98. package/src/lib/components/shared/custom-fields-form.tsx +14 -9
  99. package/src/lib/components/shared/language-selector.tsx +14 -6
  100. package/src/lib/components/shared/multi-select.tsx +1 -1
  101. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  102. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +4 -4
  103. package/src/lib/components/ui/carousel.tsx +2 -2
  104. package/src/lib/components/ui/chart.tsx +1 -1
  105. package/src/lib/components/ui/context-menu.tsx +1 -1
  106. package/src/lib/components/ui/drawer.tsx +1 -1
  107. package/src/lib/components/ui/grid-layout.tsx +1 -1
  108. package/src/lib/components/ui/input-group.tsx +1 -0
  109. package/src/lib/components/ui/input-otp.tsx +1 -1
  110. package/src/lib/components/ui/menubar.tsx +1 -1
  111. package/src/lib/components/ui/navigation-menu.tsx +1 -1
  112. package/src/lib/components/ui/progress.tsx +1 -1
  113. package/src/lib/components/ui/radio-group.tsx +1 -1
  114. package/src/lib/components/ui/resizable.tsx +1 -1
  115. package/src/lib/components/ui/select.tsx +1 -1
  116. package/src/lib/components/ui/slider.tsx +1 -1
  117. package/src/lib/components/ui/toggle-group.tsx +2 -2
  118. package/src/lib/components/ui/toggle.tsx +1 -1
  119. package/src/lib/framework/component-registry/component-registry.tsx +2 -6
  120. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +907 -1
  121. package/src/lib/framework/document-introspection/add-custom-fields.ts +248 -119
  122. package/src/lib/framework/extension-api/display-component-extensions.tsx +4 -3
  123. package/src/lib/framework/extension-api/logic/detail-forms.ts +0 -13
  124. package/src/lib/framework/extension-api/logic/navigation.ts +1 -1
  125. package/src/lib/framework/extension-api/types/data-table.ts +4 -2
  126. package/src/lib/framework/extension-api/types/layout.ts +34 -1
  127. package/src/lib/framework/extension-api/types/navigation.ts +7 -2
  128. package/src/lib/framework/form-engine/use-generated-form.tsx +7 -1
  129. package/src/lib/framework/history-entry/history-entry.tsx +1 -1
  130. package/src/lib/framework/layout-engine/action-bar-item-wrapper.tsx +185 -0
  131. package/src/lib/framework/layout-engine/dev-mode-button.tsx +15 -13
  132. package/src/lib/framework/layout-engine/location-wrapper.tsx +3 -1
  133. package/src/lib/framework/layout-engine/page-layout.spec.tsx +138 -0
  134. package/src/lib/framework/layout-engine/page-layout.tsx +294 -69
  135. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +1 -1
  136. package/src/lib/framework/page/detail-page-route-loader.tsx +1 -1
  137. package/src/lib/framework/page/page-api.ts +1 -1
  138. package/src/lib/framework/page/use-detail-page.ts +4 -2
  139. package/src/lib/framework/page/use-extended-router.tsx +20 -16
  140. package/src/lib/framework/registry/registry-types.ts +2 -1
  141. package/src/lib/graphql/api.ts +3 -8
  142. package/src/lib/graphql/graphql-env.d.ts +29 -10
  143. package/src/lib/hooks/use-permissions.ts +3 -3
  144. package/src/lib/hooks/use-sorted-languages.ts +41 -0
  145. package/src/lib/index.ts +1 -0
  146. package/src/lib/lib/load-i18n-messages.ts +4 -1
  147. package/src/lib/providers/channel-provider.tsx +11 -7
  148. package/src/lib/utils/config-utils.ts +19 -0
  149. package/src/lib/virtual.d.ts +3 -0
  150. package/LICENSE.md +0 -42
  151. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +0 -129
  152. /package/src/{app/routes/_authenticated/_global-settings → lib}/utils/global-languages.ts +0 -0
@@ -3,7 +3,7 @@ import { graphql } from 'gql.tada';
3
3
  import { DocumentNode, FieldNode, FragmentDefinitionNode, Kind, print } from 'graphql';
4
4
  import { beforeEach, describe, expect, it } from 'vitest';
5
5
 
6
- import { addCustomFields } from './add-custom-fields.js';
6
+ import { addCustomFields, addCustomFieldsToFragment } from './add-custom-fields.js';
7
7
 
8
8
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
9
9
  describe('addCustomFields()', () => {
@@ -549,4 +549,910 @@ describe('addCustomFields()', () => {
549
549
  expect(printed).not.toMatch(/product\s*\{[^}]*customFields/s);
550
550
  });
551
551
  });
552
+
553
+ describe('includeNestedFragments option', () => {
554
+ it('Should add custom fields to nested fragments when explicitly included', () => {
555
+ const assetFragment = graphql(`
556
+ fragment Asset on Asset {
557
+ id
558
+ preview
559
+ }
560
+ `);
561
+
562
+ const documentNode = graphql(
563
+ `
564
+ query ProductList($options: ProductListOptions) {
565
+ products(options: $options) {
566
+ items {
567
+ id
568
+ name
569
+ featuredAsset {
570
+ ...Asset
571
+ }
572
+ }
573
+ }
574
+ }
575
+ `,
576
+ [assetFragment],
577
+ );
578
+
579
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
580
+ customFieldsConfig.set('Product', [{ name: 'productCustomField', type: 'string', list: false }]);
581
+ customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
582
+
583
+ const result = addCustomFields(documentNode, {
584
+ customFieldsMap: customFieldsConfig,
585
+ includeNestedFragments: ['Asset'], // Explicitly include the nested Asset fragment
586
+ });
587
+ const printed = print(result);
588
+
589
+ // Should add customFields to Product (top-level)
590
+ expect(printed).toContain('productCustomField');
591
+
592
+ // Should ALSO add customFields to Asset (nested, but explicitly included)
593
+ expect(printed).toContain('fragment Asset on Asset');
594
+ expect(printed).toContain('assetCustomField');
595
+ });
596
+
597
+ it('Should handle multiple nested fragments in includeNestedFragments', () => {
598
+ const assetFragment = graphql(`
599
+ fragment Asset on Asset {
600
+ id
601
+ preview
602
+ }
603
+ `);
604
+
605
+ const orderLineFragment = graphql(`
606
+ fragment OrderLine on OrderLine {
607
+ id
608
+ quantity
609
+ }
610
+ `);
611
+
612
+ const documentNode = graphql(
613
+ `
614
+ query GetOrder($id: ID!) {
615
+ order(id: $id) {
616
+ id
617
+ code
618
+ lines {
619
+ ...OrderLine
620
+ }
621
+ featuredAsset {
622
+ ...Asset
623
+ }
624
+ }
625
+ }
626
+ `,
627
+ [orderLineFragment, assetFragment],
628
+ );
629
+
630
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
631
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
632
+ customFieldsConfig.set('OrderLine', [
633
+ { name: 'orderLineCustomField', type: 'string', list: false },
634
+ ]);
635
+ customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
636
+
637
+ const result = addCustomFields(documentNode, {
638
+ customFieldsMap: customFieldsConfig,
639
+ includeNestedFragments: ['OrderLine', 'Asset'], // Include both nested fragments
640
+ });
641
+ const printed = print(result);
642
+
643
+ // Should add customFields to all three
644
+ expect(printed).toContain('orderCustomField');
645
+ expect(printed).toContain('orderLineCustomField');
646
+ expect(printed).toContain('assetCustomField');
647
+ });
648
+
649
+ it('Should only add custom fields to specified nested fragments, not all nested fragments', () => {
650
+ const assetFragment = graphql(`
651
+ fragment Asset on Asset {
652
+ id
653
+ preview
654
+ }
655
+ `);
656
+
657
+ const orderLineFragment = graphql(`
658
+ fragment OrderLine on OrderLine {
659
+ id
660
+ quantity
661
+ }
662
+ `);
663
+
664
+ const documentNode = graphql(
665
+ `
666
+ query GetOrder($id: ID!) {
667
+ order(id: $id) {
668
+ id
669
+ lines {
670
+ ...OrderLine
671
+ }
672
+ featuredAsset {
673
+ ...Asset
674
+ }
675
+ }
676
+ }
677
+ `,
678
+ [orderLineFragment, assetFragment],
679
+ );
680
+
681
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
682
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
683
+ customFieldsConfig.set('OrderLine', [
684
+ { name: 'orderLineCustomField', type: 'string', list: false },
685
+ ]);
686
+ customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
687
+
688
+ const result = addCustomFields(documentNode, {
689
+ customFieldsMap: customFieldsConfig,
690
+ includeNestedFragments: ['OrderLine'], // Only include OrderLine, not Asset
691
+ });
692
+ const printed = print(result);
693
+
694
+ // Should add customFields to Order (top-level) and OrderLine (explicitly included)
695
+ expect(printed).toContain('orderCustomField');
696
+ expect(printed).toContain('orderLineCustomField');
697
+
698
+ // Should NOT add customFields to Asset (nested and not included)
699
+ expect(printed).not.toContain('assetCustomField');
700
+ });
701
+
702
+ it('Works with the timing issue - called later when globalCustomFieldsMap is populated', () => {
703
+ const orderLineFragment = graphql(`
704
+ fragment OrderLine on OrderLine {
705
+ id
706
+ quantity
707
+ }
708
+ `);
709
+
710
+ const orderDetailFragment = graphql(
711
+ `
712
+ fragment OrderDetail on Order {
713
+ id
714
+ code
715
+ lines {
716
+ ...OrderLine
717
+ }
718
+ }
719
+ `,
720
+ [orderLineFragment],
721
+ );
722
+
723
+ const orderDetailDocument = graphql(
724
+ `
725
+ query GetOrder($id: ID!) {
726
+ order(id: $id) {
727
+ ...OrderDetail
728
+ }
729
+ }
730
+ `,
731
+ [orderDetailFragment],
732
+ );
733
+
734
+ // Initially, globalCustomFieldsMap is empty (simulating module load time)
735
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
736
+ // Documents are created...
737
+
738
+ // Later, when server config is loaded and custom fields are available
739
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
740
+ customFieldsConfig.set('OrderLine', [
741
+ { name: 'orderLineCustomField', type: 'string', list: false },
742
+ ]);
743
+
744
+ // Now when addCustomFields is called (e.g., in a component), it has access to custom fields
745
+ const result = addCustomFields(orderDetailDocument, {
746
+ customFieldsMap: customFieldsConfig,
747
+ includeNestedFragments: ['OrderLine'], // Explicitly include nested OrderLine fragment
748
+ });
749
+ const printed = print(result);
750
+
751
+ // Should add customFields to both Order and OrderLine
752
+ expect(printed).toContain('orderCustomField');
753
+ expect(printed).toContain('orderLineCustomField');
754
+ });
755
+ });
756
+ });
757
+
758
+ describe('addCustomFieldsToFragment()', () => {
759
+ /**
760
+ * Normalizes the indentation of a string to make it easier to compare with the expected output
761
+ */
762
+ function normalizeIndentation(str: string): string {
763
+ const lines = str.replace(/ /g, ' ').split('\n');
764
+ const indentLength = lines[1].search(/\S|$/); // Find the first non-whitespace character
765
+ return lines
766
+ .map(line => line.slice(indentLength))
767
+ .join('\n')
768
+ .trim()
769
+ .replace(/"/g, '');
770
+ }
771
+
772
+ describe('Basic functionality', () => {
773
+ it('Adds customFields to a simple fragment', () => {
774
+ const fragmentDocument = graphql(`
775
+ fragment Product on Product {
776
+ id
777
+ name
778
+ }
779
+ `);
780
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
781
+ customFieldsConfig.set('Product', [
782
+ { name: 'custom1', type: 'string', list: false },
783
+ { name: 'custom2', type: 'boolean', list: false },
784
+ ]);
785
+
786
+ const result = addCustomFieldsToFragment(fragmentDocument, {
787
+ customFieldsMap: customFieldsConfig,
788
+ });
789
+
790
+ expect(print(result)).toBe(
791
+ normalizeIndentation(`
792
+ fragment Product on Product {
793
+ id
794
+ name
795
+ customFields {
796
+ custom1
797
+ custom2
798
+ }
799
+ }
800
+ `),
801
+ );
802
+ });
803
+
804
+ it('Adds customFields with includeCustomFields filter', () => {
805
+ const fragmentDocument = graphql(`
806
+ fragment Product on Product {
807
+ id
808
+ name
809
+ }
810
+ `);
811
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
812
+ customFieldsConfig.set('Product', [
813
+ { name: 'custom1', type: 'string', list: false },
814
+ { name: 'custom2', type: 'boolean', list: false },
815
+ { name: 'custom3', type: 'int', list: false },
816
+ ]);
817
+
818
+ const result = addCustomFieldsToFragment(fragmentDocument, {
819
+ customFieldsMap: customFieldsConfig,
820
+ includeCustomFields: ['custom1', 'custom3'],
821
+ });
822
+
823
+ expect(print(result)).toBe(
824
+ normalizeIndentation(`
825
+ fragment Product on Product {
826
+ id
827
+ name
828
+ customFields {
829
+ custom1
830
+ custom3
831
+ }
832
+ }
833
+ `),
834
+ );
835
+ });
836
+
837
+ it('Handles fragment with no custom fields configured', () => {
838
+ const fragmentDocument = graphql(`
839
+ fragment Product on Product {
840
+ id
841
+ name
842
+ }
843
+ `);
844
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
845
+
846
+ const result = addCustomFieldsToFragment(fragmentDocument, {
847
+ customFieldsMap: customFieldsConfig,
848
+ });
849
+
850
+ // Should return the fragment unchanged
851
+ expect(print(result)).toBe(
852
+ normalizeIndentation(`
853
+ fragment Product on Product {
854
+ id
855
+ name
856
+ }
857
+ `),
858
+ );
859
+ });
860
+ });
861
+
862
+ describe('Validation', () => {
863
+ it('Throws error when given a query document', () => {
864
+ const documentNode = graphql(`
865
+ query GetProduct {
866
+ product {
867
+ id
868
+ name
869
+ }
870
+ }
871
+ `);
872
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
873
+ customFieldsConfig.set('Product', [{ name: 'custom', type: 'string', list: false }]);
874
+
875
+ expect(() =>
876
+ addCustomFieldsToFragment(documentNode, { customFieldsMap: customFieldsConfig }),
877
+ ).toThrow('expects a fragment-only document');
878
+ });
879
+
880
+ it('Only modifies the first fragment when multiple fragments are present', () => {
881
+ const productFragment = graphql(`
882
+ fragment Product on Product {
883
+ id
884
+ }
885
+ `);
886
+ const variantFragment = graphql(`
887
+ fragment Variant on ProductVariant {
888
+ id
889
+ }
890
+ `);
891
+
892
+ // Create a document with both fragments (Product first, then Variant)
893
+ const multiFragmentDoc = {
894
+ kind: Kind.DOCUMENT,
895
+ definitions: [...productFragment.definitions, ...variantFragment.definitions],
896
+ } as DocumentNode;
897
+
898
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
899
+ customFieldsConfig.set('Product', [{ name: 'productCustom', type: 'string', list: false }]);
900
+ customFieldsConfig.set('ProductVariant', [
901
+ { name: 'variantCustom', type: 'string', list: false },
902
+ ]);
903
+
904
+ const result = addCustomFieldsToFragment(multiFragmentDoc, {
905
+ customFieldsMap: customFieldsConfig,
906
+ });
907
+ const printed = print(result);
908
+
909
+ // Should add customFields to Product (first fragment)
910
+ expect(printed).toContain('fragment Product on Product');
911
+ expect(printed).toContain('productCustom');
912
+
913
+ // Should NOT add customFields to Variant (dependency fragment)
914
+ expect(printed).toContain('fragment Variant on ProductVariant');
915
+ expect(printed).not.toContain('variantCustom');
916
+ });
917
+
918
+ it('Throws error when given an empty document', () => {
919
+ const emptyDoc = {
920
+ kind: Kind.DOCUMENT,
921
+ definitions: [],
922
+ } as DocumentNode;
923
+
924
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
925
+
926
+ expect(() =>
927
+ addCustomFieldsToFragment(emptyDoc, { customFieldsMap: customFieldsConfig }),
928
+ ).toThrow('expects a document with at least one fragment definition');
929
+ });
930
+ });
931
+
932
+ describe('Advanced field types', () => {
933
+ it('Handles relation custom fields', () => {
934
+ const fragmentDocument = graphql(`
935
+ fragment Product on Product {
936
+ id
937
+ name
938
+ }
939
+ `);
940
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
941
+ customFieldsConfig.set('Product', [
942
+ {
943
+ name: 'relatedProduct',
944
+ type: 'relation',
945
+ list: false,
946
+ scalarFields: ['id', 'name', 'slug'],
947
+ },
948
+ ]);
949
+
950
+ const result = addCustomFieldsToFragment(fragmentDocument, {
951
+ customFieldsMap: customFieldsConfig,
952
+ });
953
+
954
+ expect(print(result)).toBe(
955
+ normalizeIndentation(`
956
+ fragment Product on Product {
957
+ id
958
+ name
959
+ customFields {
960
+ relatedProduct {
961
+ id
962
+ name
963
+ slug
964
+ }
965
+ }
966
+ }
967
+ `),
968
+ );
969
+ });
970
+
971
+ it('Handles struct custom fields', () => {
972
+ const fragmentDocument = graphql(`
973
+ fragment Product on Product {
974
+ id
975
+ name
976
+ }
977
+ `);
978
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
979
+ customFieldsConfig.set('Product', [
980
+ {
981
+ name: 'dimensions',
982
+ type: 'struct',
983
+ list: false,
984
+ fields: [
985
+ { name: 'width', type: 'int' },
986
+ { name: 'height', type: 'int' },
987
+ { name: 'depth', type: 'int' },
988
+ ],
989
+ },
990
+ ]);
991
+
992
+ const result = addCustomFieldsToFragment(fragmentDocument, {
993
+ customFieldsMap: customFieldsConfig,
994
+ });
995
+
996
+ expect(print(result)).toBe(
997
+ normalizeIndentation(`
998
+ fragment Product on Product {
999
+ id
1000
+ name
1001
+ customFields {
1002
+ dimensions {
1003
+ width
1004
+ height
1005
+ depth
1006
+ }
1007
+ }
1008
+ }
1009
+ `),
1010
+ );
1011
+ });
1012
+
1013
+ it('Handles localized custom fields in translations', () => {
1014
+ const fragmentDocument = graphql(`
1015
+ fragment Product on Product {
1016
+ id
1017
+ translations {
1018
+ languageCode
1019
+ name
1020
+ }
1021
+ }
1022
+ `);
1023
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1024
+ customFieldsConfig.set('Product', [
1025
+ { name: 'customDescription', type: 'localeString', list: false },
1026
+ { name: 'customSeoTitle', type: 'localeText', list: false },
1027
+ ]);
1028
+
1029
+ const result = addCustomFieldsToFragment(fragmentDocument, {
1030
+ customFieldsMap: customFieldsConfig,
1031
+ });
1032
+
1033
+ const printed = print(result);
1034
+ // Should add localized fields to translations
1035
+ expect(printed).toContain('translations {');
1036
+ expect(printed).toMatch(/translations\s*\{[^}]*customFields/s);
1037
+
1038
+ const fragmentDef = result.definitions[0] as FragmentDefinitionNode;
1039
+ const translationsField = fragmentDef.selectionSet.selections.find(
1040
+ s => s.kind === Kind.FIELD && s.name.value === 'translations',
1041
+ ) as FieldNode;
1042
+ const customFieldsInTranslations = translationsField.selectionSet!.selections.find(
1043
+ s => s.kind === Kind.FIELD && s.name.value === 'customFields',
1044
+ ) as FieldNode;
1045
+
1046
+ expect(customFieldsInTranslations).toBeTruthy();
1047
+ expect(customFieldsInTranslations.selectionSet!.selections.length).toBe(2);
1048
+ });
1049
+ });
1050
+
1051
+ describe('Special type handling', () => {
1052
+ it('Handles OrderAddress as alias of Address', () => {
1053
+ const fragmentDocument = graphql(`
1054
+ fragment OrderAddress on OrderAddress {
1055
+ id
1056
+ streetLine1
1057
+ }
1058
+ `);
1059
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1060
+ // Custom fields are configured for Address, not OrderAddress
1061
+ customFieldsConfig.set('Address', [{ name: 'buildingNumber', type: 'string', list: false }]);
1062
+
1063
+ const result = addCustomFieldsToFragment(fragmentDocument, {
1064
+ customFieldsMap: customFieldsConfig,
1065
+ });
1066
+
1067
+ // Should still add custom fields because OrderAddress is aliased to Address
1068
+ expect(print(result)).toContain('customFields {');
1069
+ expect(print(result)).toContain('buildingNumber');
1070
+ });
1071
+
1072
+ it('Handles Country as alias of Region', () => {
1073
+ const fragmentDocument = graphql(`
1074
+ fragment Country on Country {
1075
+ id
1076
+ name
1077
+ }
1078
+ `);
1079
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1080
+ // Custom fields are configured for Region, not Country
1081
+ customFieldsConfig.set('Region', [{ name: 'regionCode', type: 'string', list: false }]);
1082
+
1083
+ const result = addCustomFieldsToFragment(fragmentDocument, {
1084
+ customFieldsMap: customFieldsConfig,
1085
+ });
1086
+
1087
+ // Should still add custom fields because Country is aliased to Region
1088
+ expect(print(result)).toContain('customFields {');
1089
+ expect(print(result)).toContain('regionCode');
1090
+ });
1091
+ });
1092
+
1093
+ describe('Memoization', () => {
1094
+ it('Returns the same instance for the same inputs', () => {
1095
+ const fragmentDocument = graphql(`
1096
+ fragment Product on Product {
1097
+ id
1098
+ name
1099
+ }
1100
+ `);
1101
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1102
+ customFieldsConfig.set('Product', [{ name: 'custom', type: 'string', list: false }]);
1103
+
1104
+ const result1 = addCustomFieldsToFragment(fragmentDocument, {
1105
+ customFieldsMap: customFieldsConfig,
1106
+ });
1107
+ const result2 = addCustomFieldsToFragment(fragmentDocument, {
1108
+ customFieldsMap: customFieldsConfig,
1109
+ });
1110
+
1111
+ // Should return the exact same instance (identity equality)
1112
+ expect(result1).toBe(result2);
1113
+ });
1114
+
1115
+ it('Returns different instances for different options', () => {
1116
+ const fragmentDocument = graphql(`
1117
+ fragment Product on Product {
1118
+ id
1119
+ name
1120
+ }
1121
+ `);
1122
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1123
+ customFieldsConfig.set('Product', [
1124
+ { name: 'custom1', type: 'string', list: false },
1125
+ { name: 'custom2', type: 'boolean', list: false },
1126
+ ]);
1127
+
1128
+ const result1 = addCustomFieldsToFragment(fragmentDocument, {
1129
+ customFieldsMap: customFieldsConfig,
1130
+ includeCustomFields: ['custom1'],
1131
+ });
1132
+ const result2 = addCustomFieldsToFragment(fragmentDocument, {
1133
+ customFieldsMap: customFieldsConfig,
1134
+ includeCustomFields: ['custom2'],
1135
+ });
1136
+
1137
+ // Should return different instances for different options
1138
+ expect(result1).not.toBe(result2);
1139
+ expect(print(result1)).toContain('custom1');
1140
+ expect(print(result1)).not.toContain('custom2');
1141
+ expect(print(result2)).toContain('custom2');
1142
+ expect(print(result2)).not.toContain('custom1');
1143
+ });
1144
+ });
1145
+
1146
+ describe('Fragment spreads handling', () => {
1147
+ it('Should only add custom fields to the top-level fragment, not to referenced fragments', () => {
1148
+ const orderLineFragment = graphql(`
1149
+ fragment OrderLine on OrderLine {
1150
+ id
1151
+ quantity
1152
+ }
1153
+ `);
1154
+
1155
+ const orderDetailFragment = graphql(
1156
+ `
1157
+ fragment OrderDetail on Order {
1158
+ id
1159
+ code
1160
+ lines {
1161
+ ...OrderLine
1162
+ }
1163
+ }
1164
+ `,
1165
+ [orderLineFragment],
1166
+ );
1167
+
1168
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1169
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
1170
+ customFieldsConfig.set('OrderLine', [
1171
+ { name: 'orderLineCustomField', type: 'string', list: false },
1172
+ ]);
1173
+
1174
+ // Apply to the OrderDetail fragment only
1175
+ const result = addCustomFieldsToFragment(orderDetailFragment, {
1176
+ customFieldsMap: customFieldsConfig,
1177
+ });
1178
+
1179
+ const printed = print(result);
1180
+
1181
+ // Should add customFields to OrderDetail (top-level fragment)
1182
+ expect(printed).toContain('fragment OrderDetail on Order');
1183
+ expect(printed).toContain('orderCustomField');
1184
+
1185
+ // Should include the OrderLine fragment definition (dependency) but NOT add customFields to it
1186
+ expect(printed).toContain('...OrderLine');
1187
+ expect(printed).toContain('fragment OrderLine on OrderLine');
1188
+ expect(printed).not.toContain('orderLineCustomField');
1189
+ });
1190
+
1191
+ it('Should work with deeply nested fragment spreads', () => {
1192
+ const assetFragment = graphql(`
1193
+ fragment Asset on Asset {
1194
+ id
1195
+ preview
1196
+ }
1197
+ `);
1198
+
1199
+ const orderLineFragment = graphql(
1200
+ `
1201
+ fragment OrderLine on OrderLine {
1202
+ id
1203
+ quantity
1204
+ featuredAsset {
1205
+ ...Asset
1206
+ }
1207
+ }
1208
+ `,
1209
+ [assetFragment],
1210
+ );
1211
+
1212
+ const orderDetailFragment = graphql(
1213
+ `
1214
+ fragment OrderDetail on Order {
1215
+ id
1216
+ code
1217
+ lines {
1218
+ ...OrderLine
1219
+ }
1220
+ }
1221
+ `,
1222
+ [orderLineFragment],
1223
+ );
1224
+
1225
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1226
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
1227
+ customFieldsConfig.set('OrderLine', [
1228
+ { name: 'orderLineCustomField', type: 'string', list: false },
1229
+ ]);
1230
+ customFieldsConfig.set('Asset', [{ name: 'assetCustomField', type: 'string', list: false }]);
1231
+
1232
+ const result = addCustomFieldsToFragment(orderDetailFragment, {
1233
+ customFieldsMap: customFieldsConfig,
1234
+ });
1235
+
1236
+ const printed = print(result);
1237
+
1238
+ // Should ONLY add customFields to OrderDetail
1239
+ expect(printed).toContain('orderCustomField');
1240
+ expect(printed).not.toContain('orderLineCustomField');
1241
+ expect(printed).not.toContain('assetCustomField');
1242
+
1243
+ // Should still contain the fragment definitions (dependencies) but without custom fields
1244
+ expect(printed).toContain('...OrderLine');
1245
+ expect(printed).toContain('fragment OrderLine on OrderLine');
1246
+ expect(printed).toContain('fragment Asset on Asset');
1247
+ });
1248
+ });
1249
+
1250
+ describe('Composability with addCustomFields()', () => {
1251
+ it('Can be used inline in graphql() dependency array (like the original pattern)', () => {
1252
+ const productFragment = graphql(`
1253
+ fragment Product on Product {
1254
+ id
1255
+ name
1256
+ }
1257
+ `);
1258
+
1259
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1260
+ customFieldsConfig.set('Product', [
1261
+ { name: 'custom1', type: 'string', list: false },
1262
+ { name: 'custom2', type: 'boolean', list: false },
1263
+ ]);
1264
+
1265
+ // Use addCustomFieldsToFragment directly in the array - this is the pattern from orders.graphql.ts
1266
+ const queryDocument = graphql(
1267
+ `
1268
+ query GetProduct {
1269
+ product {
1270
+ ...Product
1271
+ }
1272
+ }
1273
+ `,
1274
+ [addCustomFieldsToFragment(productFragment, { customFieldsMap: customFieldsConfig })],
1275
+ );
1276
+
1277
+ // The query should include the modified fragment with custom fields
1278
+ const printed = print(queryDocument);
1279
+ expect(printed).toContain('customFields {');
1280
+ expect(printed).toContain('custom1');
1281
+ expect(printed).toContain('custom2');
1282
+ });
1283
+
1284
+ it('addCustomFieldsToFragment produces same result as addCustomFields for single fragments', () => {
1285
+ const productFragment = graphql(`
1286
+ fragment Product on Product {
1287
+ id
1288
+ name
1289
+ }
1290
+ `);
1291
+
1292
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1293
+ customFieldsConfig.set('Product', [
1294
+ { name: 'custom1', type: 'string', list: false },
1295
+ { name: 'custom2', type: 'boolean', list: false },
1296
+ ]);
1297
+
1298
+ const resultFromFragment = addCustomFieldsToFragment(productFragment, {
1299
+ customFieldsMap: customFieldsConfig,
1300
+ });
1301
+ const resultFromFull = addCustomFields(productFragment, { customFieldsMap: customFieldsConfig });
1302
+
1303
+ // Both should produce the same output
1304
+ expect(print(resultFromFragment)).toBe(print(resultFromFull));
1305
+ });
1306
+
1307
+ it('Works with fragments that have dependencies when used inline', () => {
1308
+ const orderLineFragment = graphql(`
1309
+ fragment OrderLine on OrderLine {
1310
+ id
1311
+ quantity
1312
+ }
1313
+ `);
1314
+
1315
+ const orderDetailFragment = graphql(
1316
+ `
1317
+ fragment OrderDetail on Order {
1318
+ id
1319
+ code
1320
+ lines {
1321
+ ...OrderLine
1322
+ }
1323
+ }
1324
+ `,
1325
+ [orderLineFragment],
1326
+ );
1327
+
1328
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1329
+ customFieldsConfig.set('Order', [{ name: 'orderCustomField', type: 'string', list: false }]);
1330
+ customFieldsConfig.set('OrderLine', [
1331
+ { name: 'orderLineCustomField', type: 'string', list: false },
1332
+ ]);
1333
+
1334
+ // This is exactly the pattern used in orders.graphql.ts
1335
+ const queryDocument = graphql(
1336
+ `
1337
+ query GetOrder($id: ID!) {
1338
+ order(id: $id) {
1339
+ ...OrderDetail
1340
+ }
1341
+ }
1342
+ `,
1343
+ [addCustomFieldsToFragment(orderDetailFragment, { customFieldsMap: customFieldsConfig })],
1344
+ );
1345
+
1346
+ const printed = print(queryDocument);
1347
+
1348
+ // Should add custom fields to OrderDetail
1349
+ expect(printed).toContain('fragment OrderDetail on Order');
1350
+ expect(printed).toContain('orderCustomField');
1351
+
1352
+ // Should NOT add custom fields to OrderLine (dependency)
1353
+ expect(printed).toContain('fragment OrderLine on OrderLine');
1354
+ expect(printed).not.toContain('orderLineCustomField');
1355
+
1356
+ // Verify the query structure is correct
1357
+ expect(printed).toContain('query GetOrder');
1358
+ expect(printed).toContain('order(id: $id)');
1359
+ expect(printed).toContain('...OrderDetail');
1360
+ });
1361
+
1362
+ it('Can be used to compose fragments in query documents', () => {
1363
+ const productFragment = graphql(`
1364
+ fragment Product on Product {
1365
+ id
1366
+ name
1367
+ }
1368
+ `);
1369
+
1370
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1371
+ customFieldsConfig.set('Product', [
1372
+ { name: 'custom1', type: 'string', list: false },
1373
+ { name: 'custom2', type: 'boolean', list: false },
1374
+ ]);
1375
+
1376
+ // Use addCustomFieldsToFragment to modify the fragment
1377
+ const modifiedFragment = addCustomFieldsToFragment(productFragment, {
1378
+ customFieldsMap: customFieldsConfig,
1379
+ });
1380
+
1381
+ // Then compose it into a query
1382
+ const queryDocument = graphql(
1383
+ `
1384
+ query GetProduct {
1385
+ product {
1386
+ ...Product
1387
+ }
1388
+ }
1389
+ `,
1390
+ [modifiedFragment],
1391
+ );
1392
+
1393
+ // The query should include the modified fragment with custom fields
1394
+ const printed = print(queryDocument);
1395
+ expect(printed).toContain('customFields {');
1396
+ expect(printed).toContain('custom1');
1397
+ expect(printed).toContain('custom2');
1398
+ });
1399
+
1400
+ it('Can selectively modify different fragments with different custom fields', () => {
1401
+ const productFragment = graphql(`
1402
+ fragment Product on Product {
1403
+ id
1404
+ name
1405
+ }
1406
+ `);
1407
+
1408
+ const variantFragment = graphql(`
1409
+ fragment Variant on ProductVariant {
1410
+ id
1411
+ sku
1412
+ }
1413
+ `);
1414
+
1415
+ const customFieldsConfig = new Map<string, CustomFieldConfig[]>();
1416
+ customFieldsConfig.set('Product', [
1417
+ { name: 'productCustom1', type: 'string', list: false },
1418
+ { name: 'productCustom2', type: 'boolean', list: false },
1419
+ ]);
1420
+ customFieldsConfig.set('ProductVariant', [
1421
+ { name: 'variantCustom1', type: 'string', list: false },
1422
+ ]);
1423
+
1424
+ // Selectively modify each fragment with different custom fields
1425
+ const modifiedProductFragment = addCustomFieldsToFragment(productFragment, {
1426
+ customFieldsMap: customFieldsConfig,
1427
+ includeCustomFields: ['productCustom1'], // Only include productCustom1
1428
+ });
1429
+
1430
+ const modifiedVariantFragment = addCustomFieldsToFragment(variantFragment, {
1431
+ customFieldsMap: customFieldsConfig,
1432
+ includeCustomFields: ['variantCustom1'],
1433
+ });
1434
+
1435
+ // Compose into a query
1436
+ const queryDocument = graphql(
1437
+ `
1438
+ query GetProductWithVariants {
1439
+ product {
1440
+ ...Product
1441
+ variants {
1442
+ ...Variant
1443
+ }
1444
+ }
1445
+ }
1446
+ `,
1447
+ [modifiedProductFragment, modifiedVariantFragment],
1448
+ );
1449
+
1450
+ const printed = print(queryDocument);
1451
+ // Product fragment should have only productCustom1
1452
+ expect(printed).toContain('productCustom1');
1453
+ expect(printed).not.toContain('productCustom2');
1454
+ // Variant fragment should have variantCustom1
1455
+ expect(printed).toContain('variantCustom1');
1456
+ });
1457
+ });
552
1458
  });