@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202510012036

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 (295) hide show
  1. package/README.md +4 -0
  2. package/dist/plugin/api/api-extensions.js +11 -14
  3. package/dist/plugin/api/metrics.resolver.d.ts +2 -2
  4. package/dist/plugin/api/metrics.resolver.js +2 -2
  5. package/dist/plugin/config/metrics-strategies.d.ts +9 -9
  6. package/dist/plugin/config/metrics-strategies.js +6 -6
  7. package/dist/plugin/constants.d.ts +2 -0
  8. package/dist/plugin/constants.js +3 -1
  9. package/dist/plugin/dashboard.plugin.js +13 -0
  10. package/dist/plugin/service/metrics.service.d.ts +3 -3
  11. package/dist/plugin/service/metrics.service.js +37 -53
  12. package/dist/plugin/types.d.ts +9 -12
  13. package/dist/plugin/types.js +7 -11
  14. package/dist/vite/vite-plugin-config.js +13 -9
  15. package/dist/vite/vite-plugin-translations.d.ts +22 -0
  16. package/dist/vite/vite-plugin-translations.js +66 -0
  17. package/dist/vite/vite-plugin-vendure-dashboard.js +10 -8
  18. package/lingui.config.js +25 -2
  19. package/package.json +159 -156
  20. package/src/app/app-providers.tsx +0 -4
  21. package/src/app/common/delete-bulk-action.tsx +6 -5
  22. package/src/app/common/duplicate-bulk-action.tsx +4 -5
  23. package/src/app/common/duplicate-entity-dialog.tsx +1 -1
  24. package/src/app/common/set-document-direction.ts +7 -0
  25. package/src/app/main.tsx +50 -17
  26. package/src/app/routes/_authenticated/_administrators/administrators.tsx +8 -6
  27. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +17 -6
  28. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +2 -2
  29. package/src/app/routes/_authenticated/_assets/assets.tsx +1 -1
  30. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +4 -4
  31. package/src/app/routes/_authenticated/_assets/components/asset-bulk-actions.tsx +8 -6
  32. package/src/app/routes/_authenticated/_assets/components/asset-tag-filter.tsx +1 -1
  33. package/src/app/routes/_authenticated/_assets/components/asset-tags-editor.tsx +1 -1
  34. package/src/app/routes/_authenticated/_assets/components/manage-tags-dialog.tsx +3 -8
  35. package/src/app/routes/_authenticated/_channels/channels.tsx +3 -6
  36. package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +5 -5
  37. package/src/app/routes/_authenticated/_collections/collections.tsx +10 -6
  38. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +16 -5
  39. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
  40. package/src/app/routes/_authenticated/_collections/components/collection-contents-sheet.tsx +1 -1
  41. package/src/app/routes/_authenticated/_collections/components/move-collections-dialog.tsx +6 -6
  42. package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
  43. package/src/app/routes/_authenticated/_countries/countries.tsx +2 -3
  44. package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +4 -4
  45. package/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-sheet.tsx +1 -1
  46. package/src/app/routes/_authenticated/_customer-groups/components/customer-group-members-table.tsx +4 -4
  47. package/src/app/routes/_authenticated/_customer-groups/customer-groups.tsx +2 -4
  48. package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +13 -6
  49. package/src/app/routes/_authenticated/_customers/components/customer-address-card.tsx +8 -8
  50. package/src/app/routes/_authenticated/_customers/components/customer-address-form.tsx +3 -3
  51. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +1 -1
  52. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +1 -1
  53. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +1 -1
  54. package/src/app/routes/_authenticated/_customers/components/customer-history/use-customer-history.ts +1 -1
  55. package/src/app/routes/_authenticated/_customers/components/customer-status-badge.tsx +1 -1
  56. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +4 -0
  57. package/src/app/routes/_authenticated/_customers/customers.tsx +23 -11
  58. package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +10 -8
  59. package/src/app/routes/_authenticated/_facets/components/edit-facet-value.tsx +1 -1
  60. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +6 -5
  61. package/src/app/routes/_authenticated/_facets/components/facet-values-sheet.tsx +1 -1
  62. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +1 -1
  63. package/src/app/routes/_authenticated/_facets/facets.tsx +5 -5
  64. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +7 -5
  65. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +18 -6
  66. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +5 -5
  67. package/src/app/routes/_authenticated/_orders/components/add-manual-payment-dialog.tsx +19 -21
  68. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +1 -1
  69. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +22 -22
  70. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +6 -6
  71. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +15 -9
  72. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +1 -1
  73. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +11 -9
  74. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +1 -1
  75. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +1 -1
  76. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +1 -1
  77. package/src/app/routes/_authenticated/_orders/components/order-history/use-order-history.ts +1 -1
  78. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +1 -1
  79. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +4 -4
  80. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +1 -1
  81. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +27 -27
  82. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +2 -2
  83. package/src/app/routes/_authenticated/_orders/components/order-tax-summary.tsx +1 -1
  84. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +26 -20
  85. package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +3 -1
  86. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +6 -6
  87. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +1 -1
  88. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +1 -1
  89. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +3 -2
  90. package/src/app/routes/_authenticated/_orders/orders.tsx +5 -9
  91. package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +1 -1
  92. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +1 -1
  93. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +4 -4
  94. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +17 -17
  95. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +1 -1
  96. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +5 -6
  97. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +13 -6
  98. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
  99. package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +1 -1
  100. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
  101. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +9 -2
  102. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +13 -6
  103. package/src/app/routes/_authenticated/_products/components/add-option-group-dialog.tsx +5 -5
  104. package/src/app/routes/_authenticated/_products/components/add-product-variant-dialog.tsx +5 -5
  105. package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +5 -4
  106. package/src/app/routes/_authenticated/_products/components/create-product-options-dialog.tsx +9 -12
  107. package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +1 -1
  108. package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +4 -4
  109. package/src/app/routes/_authenticated/_products/components/option-groups-editor.tsx +1 -1
  110. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
  111. package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
  112. package/src/app/routes/_authenticated/_products/components/product-option-select.tsx +3 -3
  113. package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +114 -0
  114. package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
  115. package/src/app/routes/_authenticated/_products/products.graphql.ts +44 -32
  116. package/src/app/routes/_authenticated/_products/products.tsx +34 -5
  117. package/src/app/routes/_authenticated/_products/products_.$id.tsx +29 -12
  118. package/src/app/routes/_authenticated/_products/products_.$id_.variants.tsx +11 -11
  119. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +177 -0
  120. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
  121. package/src/app/routes/_authenticated/_profile/profile.tsx +4 -4
  122. package/src/app/routes/_authenticated/_promotions/promotions.tsx +2 -4
  123. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +16 -9
  124. package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +1 -1
  125. package/src/app/routes/_authenticated/_roles/roles.tsx +3 -6
  126. package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +4 -6
  127. package/src/app/routes/_authenticated/_sellers/sellers.tsx +3 -4
  128. package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +4 -4
  129. package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +5 -5
  130. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +1 -1
  131. package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +11 -11
  132. package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +1 -1
  133. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +8 -8
  134. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +1 -1
  135. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +8 -8
  136. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +4 -4
  137. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -3
  138. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +2 -2
  139. package/src/app/routes/_authenticated/_stock-locations/stock-locations.tsx +3 -4
  140. package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +13 -6
  141. package/src/app/routes/_authenticated/_system/healthchecks.tsx +10 -4
  142. package/src/app/routes/_authenticated/_system/job-queue.tsx +10 -13
  143. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +18 -16
  144. package/src/app/routes/_authenticated/_tax-categories/tax-categories.tsx +2 -4
  145. package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +13 -6
  146. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +8 -12
  147. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +6 -4
  148. package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
  149. package/src/app/routes/_authenticated/_zones/zones.tsx +4 -4
  150. package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +8 -5
  151. package/src/app/routes/_authenticated/index.tsx +46 -25
  152. package/src/app/styles.css +4 -0
  153. package/src/i18n/common-strings.ts +111 -0
  154. package/src/i18n/locales/ar.po +4777 -0
  155. package/src/i18n/locales/cs.po +4777 -0
  156. package/src/i18n/locales/de.po +4299 -1101
  157. package/src/i18n/locales/en.po +3857 -659
  158. package/src/i18n/locales/es.po +4777 -0
  159. package/src/i18n/locales/fa.po +4777 -0
  160. package/src/i18n/locales/fr.po +4777 -0
  161. package/src/i18n/locales/he.po +4777 -0
  162. package/src/i18n/locales/hr.po +4777 -0
  163. package/src/i18n/locales/it.po +4777 -0
  164. package/src/i18n/locales/ja.po +4777 -0
  165. package/src/i18n/locales/ko.po +4628 -0
  166. package/src/i18n/locales/nb.po +4777 -0
  167. package/src/i18n/locales/ne.po +4777 -0
  168. package/src/i18n/locales/nl.po +4628 -0
  169. package/src/i18n/locales/pl.po +4777 -0
  170. package/src/i18n/locales/pt_BR.po +4777 -0
  171. package/src/i18n/locales/pt_PT.po +4777 -0
  172. package/src/i18n/locales/ru.po +4777 -0
  173. package/src/i18n/locales/sv.po +4777 -0
  174. package/src/i18n/locales/tr.po +4777 -0
  175. package/src/i18n/locales/uk.po +4777 -0
  176. package/src/i18n/locales/zh_Hans.po +4777 -0
  177. package/src/i18n/locales/zh_Hant.po +4777 -0
  178. package/src/lib/components/data-display/json.tsx +16 -1
  179. package/src/lib/components/data-input/combination-mode-input.tsx +1 -1
  180. package/src/lib/components/data-input/custom-field-list-input.tsx +11 -7
  181. package/src/lib/components/data-input/customer-group-input.tsx +27 -33
  182. package/src/lib/components/data-input/datetime-input.tsx +40 -1
  183. package/src/lib/components/data-input/default-relation-input.tsx +5 -4
  184. package/src/lib/components/data-input/index.ts +3 -0
  185. package/src/lib/components/data-input/product-multi-selector-input.tsx +14 -14
  186. package/src/lib/components/data-input/relation-selector.tsx +1 -1
  187. package/src/lib/components/data-input/select-with-options.tsx +1 -1
  188. package/src/lib/components/data-input/slug-input.tsx +290 -0
  189. package/src/lib/components/data-table/add-filter-menu.tsx +17 -10
  190. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +45 -8
  191. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  192. package/src/lib/components/data-table/data-table-column-header.tsx +13 -8
  193. package/src/lib/components/data-table/data-table-context.tsx +91 -0
  194. package/src/lib/components/data-table/data-table-faceted-filter.tsx +2 -1
  195. package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
  196. package/src/lib/components/data-table/data-table-filter-dialog.tsx +1 -1
  197. package/src/lib/components/data-table/data-table-utils.ts +21 -4
  198. package/src/lib/components/data-table/data-table-view-options.tsx +21 -10
  199. package/src/lib/components/data-table/data-table.tsx +146 -94
  200. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +4 -4
  201. package/src/lib/components/data-table/global-views-bar.tsx +97 -0
  202. package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
  203. package/src/lib/components/data-table/human-readable-operator.tsx +1 -1
  204. package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
  205. package/src/lib/components/data-table/my-views-button.tsx +47 -0
  206. package/src/lib/components/data-table/refresh-button.tsx +12 -3
  207. package/src/lib/components/data-table/save-view-button.tsx +41 -0
  208. package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
  209. package/src/lib/components/data-table/use-generated-columns.tsx +13 -8
  210. package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
  211. package/src/lib/components/data-table/views-sheet.tsx +305 -0
  212. package/src/lib/components/date-range-picker.tsx +186 -0
  213. package/src/lib/components/layout/app-sidebar.tsx +3 -1
  214. package/src/lib/components/layout/channel-switcher.tsx +8 -10
  215. package/src/lib/components/layout/dev-mode-indicator.tsx +1 -1
  216. package/src/lib/components/layout/generated-breadcrumbs.tsx +10 -8
  217. package/src/lib/components/layout/language-dialog.tsx +34 -13
  218. package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
  219. package/src/lib/components/layout/nav-main.tsx +23 -13
  220. package/src/lib/components/layout/nav-user.tsx +19 -23
  221. package/src/lib/components/login/login-form.tsx +1 -1
  222. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +4 -4
  223. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +1 -1
  224. package/src/lib/components/shared/asset/asset-gallery.tsx +15 -14
  225. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +11 -11
  226. package/src/lib/components/shared/assign-to-channel-dialog.tsx +6 -5
  227. package/src/lib/components/shared/channel-code-label.tsx +1 -1
  228. package/src/lib/components/shared/channel-selector.tsx +4 -4
  229. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +16 -14
  230. package/src/lib/components/shared/configurable-operation-selector.tsx +1 -1
  231. package/src/lib/components/shared/confirmation-dialog.tsx +8 -8
  232. package/src/lib/components/shared/country-selector.tsx +1 -1
  233. package/src/lib/components/shared/currency-selector.tsx +4 -4
  234. package/src/lib/components/shared/custom-fields-form.tsx +8 -24
  235. package/src/lib/components/shared/customer-address-form.tsx +3 -3
  236. package/src/lib/components/shared/customer-group-selector.tsx +1 -1
  237. package/src/lib/components/shared/customer-selector.tsx +1 -1
  238. package/src/lib/components/shared/error-page.tsx +1 -1
  239. package/src/lib/components/shared/facet-value-selector.tsx +10 -10
  240. package/src/lib/components/shared/history-timeline/history-note-checkbox.tsx +1 -1
  241. package/src/lib/components/shared/history-timeline/history-note-editor.tsx +1 -1
  242. package/src/lib/components/shared/history-timeline/history-note-entry.tsx +1 -1
  243. package/src/lib/components/shared/language-selector.tsx +4 -4
  244. package/src/lib/components/shared/navigation-confirmation.tsx +1 -1
  245. package/src/lib/components/shared/paginated-list-data-table.tsx +64 -34
  246. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +6 -5
  247. package/src/lib/components/shared/rich-text-editor/image-dialog.tsx +1 -1
  248. package/src/lib/components/shared/rich-text-editor/link-dialog.tsx +1 -1
  249. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +1 -1
  250. package/src/lib/components/shared/rich-text-editor/table-edit-icons.tsx +1 -1
  251. package/src/lib/components/shared/role-code-label.tsx +1 -1
  252. package/src/lib/components/shared/role-selector.tsx +4 -4
  253. package/src/lib/components/shared/seller-selector.tsx +1 -1
  254. package/src/lib/components/shared/stock-level-label.tsx +3 -5
  255. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +3 -1
  256. package/src/lib/components/shared/tax-category-selector.tsx +1 -1
  257. package/src/lib/components/shared/translatable-form-field.tsx +15 -15
  258. package/src/lib/components/shared/zone-selector.tsx +1 -1
  259. package/src/lib/components/ui/button.tsx +1 -1
  260. package/src/lib/framework/dashboard-widget/base-widget.tsx +11 -9
  261. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +35 -6
  262. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +18 -12
  263. package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
  264. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +26 -79
  265. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +35 -0
  266. package/src/lib/framework/defaults.ts +34 -63
  267. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
  268. package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
  269. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
  270. package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
  271. package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
  272. package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
  273. package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
  274. package/src/lib/framework/extension-api/types/data-table.ts +62 -4
  275. package/src/lib/framework/extension-api/types/navigation.ts +16 -0
  276. package/src/lib/framework/form-engine/utils.ts +34 -0
  277. package/src/lib/framework/layout-engine/page-layout.tsx +36 -36
  278. package/src/lib/framework/page/detail-page.tsx +10 -10
  279. package/src/lib/framework/page/list-page.tsx +289 -4
  280. package/src/lib/framework/page/use-extended-router.tsx +101 -34
  281. package/src/lib/graphql/api.ts +6 -2
  282. package/src/lib/graphql/graphql-env.d.ts +38 -26
  283. package/src/lib/hooks/use-display-locale.ts +40 -0
  284. package/src/lib/hooks/use-dynamic-translations.ts +46 -0
  285. package/src/lib/hooks/use-extended-detail-query.ts +1 -1
  286. package/src/lib/hooks/use-extended-list-query.ts +6 -1
  287. package/src/lib/hooks/use-local-format.ts +15 -1
  288. package/src/lib/hooks/use-saved-views.ts +230 -0
  289. package/src/lib/hooks/use-ui-language-loader.ts +30 -0
  290. package/src/lib/index.ts +15 -0
  291. package/src/lib/lib/load-i18n-messages.ts +17 -0
  292. package/src/lib/lib/trans.tsx +15 -11
  293. package/src/lib/providers/i18n-provider.tsx +7 -14
  294. package/src/lib/types/saved-views.ts +39 -0
  295. package/src/lib/utils/saved-views-utils.ts +40 -0
@@ -0,0 +1,1840 @@
1
+ import { DocumentNode, parse, print } from 'graphql';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { includeOnlySelectedListFields } from './include-only-selected-list-fields.js';
5
+
6
+ vi.mock('virtual:admin-api-schema', () => {
7
+ return import('./testing-utils.js').then(m => m.getMockSchemaInfo());
8
+ });
9
+
10
+ describe('includeOnlySelectedListFields', () => {
11
+ const createTestDocument = (itemsFields: string): DocumentNode => {
12
+ return parse(`
13
+ query ProductList($options: ProductListOptions) {
14
+ products(options: $options) {
15
+ items {
16
+ ${itemsFields}
17
+ }
18
+ totalItems
19
+ }
20
+ }
21
+ `);
22
+ };
23
+
24
+ const normalizeQuery = (query: string): string => {
25
+ // Remove extra whitespace and normalize for comparison
26
+ return query.replace(/\s+/g, ' ').trim();
27
+ };
28
+
29
+ describe('basic field selection', () => {
30
+ it('should return original document when no columns are selected', () => {
31
+ const document = createTestDocument('id name slug');
32
+ const result = includeOnlySelectedListFields(document, []);
33
+
34
+ expect(print(result)).toEqual(print(document));
35
+ });
36
+
37
+ it('should filter to only selected fields', () => {
38
+ const document = createTestDocument(`
39
+ id
40
+ name
41
+ slug
42
+ enabled
43
+ createdAt
44
+ updatedAt
45
+ `);
46
+
47
+ const result = includeOnlySelectedListFields(document, [
48
+ { name: 'id', isCustomField: false },
49
+ { name: 'name', isCustomField: false },
50
+ ]);
51
+
52
+ const resultQuery = normalizeQuery(print(result));
53
+ expect(resultQuery).toContain('items { id name }');
54
+ expect(resultQuery).not.toContain('slug');
55
+ expect(resultQuery).not.toContain('enabled');
56
+ expect(resultQuery).not.toContain('createdAt');
57
+ expect(resultQuery).not.toContain('updatedAt');
58
+ });
59
+
60
+ it('should handle single field selection', () => {
61
+ const document = createTestDocument(`
62
+ id
63
+ name
64
+ slug
65
+ enabled
66
+ `);
67
+
68
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
69
+
70
+ const resultQuery = normalizeQuery(print(result));
71
+ expect(resultQuery).toContain('items { name }');
72
+ expect(resultQuery).not.toContain('slug');
73
+ expect(resultQuery).not.toContain('enabled');
74
+ });
75
+
76
+ it('should preserve nested field structures', () => {
77
+ const document = createTestDocument(`
78
+ id
79
+ name
80
+ featuredAsset {
81
+ id
82
+ preview
83
+ source
84
+ }
85
+ slug
86
+ `);
87
+
88
+ const result = includeOnlySelectedListFields(document, [
89
+ { name: 'id', isCustomField: false },
90
+ { name: 'featuredAsset', isCustomField: false },
91
+ ]);
92
+
93
+ const resultQuery = normalizeQuery(print(result));
94
+ expect(resultQuery).toContain('items { id featuredAsset { id preview source } }');
95
+ expect(resultQuery).not.toContain('name');
96
+ expect(resultQuery).not.toContain('slug');
97
+ });
98
+
99
+ it('should preserve __typename if present in original', () => {
100
+ const document = createTestDocument(`
101
+ __typename
102
+ id
103
+ name
104
+ slug
105
+ `);
106
+
107
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
108
+
109
+ const resultQuery = normalizeQuery(print(result));
110
+ expect(resultQuery).toContain('__typename');
111
+ expect(resultQuery).toContain('name');
112
+ expect(resultQuery).not.toContain('slug');
113
+ });
114
+ });
115
+
116
+ describe('custom fields handling', () => {
117
+ it('should include custom fields when specified', () => {
118
+ const document = createTestDocument(`
119
+ id
120
+ name
121
+ customFields {
122
+ shortDescription
123
+ isEcoFriendly
124
+ warrantyMonths
125
+ }
126
+ `);
127
+
128
+ const result = includeOnlySelectedListFields(document, [
129
+ { name: 'id', isCustomField: false },
130
+ { name: 'shortDescription', isCustomField: true },
131
+ { name: 'isEcoFriendly', isCustomField: true },
132
+ ]);
133
+
134
+ const resultQuery = normalizeQuery(print(result));
135
+ expect(resultQuery).toContain('items { id customFields { shortDescription isEcoFriendly } }');
136
+ expect(resultQuery).not.toContain('warrantyMonths');
137
+ expect(resultQuery).not.toContain('name');
138
+ });
139
+
140
+ it('should exclude customFields entirely if no custom fields are selected', () => {
141
+ const document = createTestDocument(`
142
+ id
143
+ name
144
+ customFields {
145
+ shortDescription
146
+ isEcoFriendly
147
+ }
148
+ `);
149
+
150
+ const result = includeOnlySelectedListFields(document, [
151
+ { name: 'id', isCustomField: false },
152
+ { name: 'name', isCustomField: false },
153
+ ]);
154
+
155
+ const resultQuery = normalizeQuery(print(result));
156
+ expect(resultQuery).toContain('items { id name }');
157
+ expect(resultQuery).not.toContain('customFields');
158
+ });
159
+
160
+ it('should handle mixed regular and custom field selection', () => {
161
+ const document = createTestDocument(`
162
+ id
163
+ name
164
+ slug
165
+ enabled
166
+ customFields {
167
+ shortDescription
168
+ warrantyMonths
169
+ isEcoFriendly
170
+ }
171
+ `);
172
+
173
+ const result = includeOnlySelectedListFields(document, [
174
+ { name: 'name', isCustomField: false },
175
+ { name: 'enabled', isCustomField: false },
176
+ { name: 'shortDescription', isCustomField: true },
177
+ ]);
178
+
179
+ const resultQuery = normalizeQuery(print(result));
180
+ expect(resultQuery).toContain('name');
181
+ expect(resultQuery).toContain('enabled');
182
+ expect(resultQuery).toContain('customFields { shortDescription }');
183
+ expect(resultQuery).not.toContain('warrantyMonths');
184
+ expect(resultQuery).not.toContain('isEcoFriendly');
185
+ expect(resultQuery).not.toContain('slug');
186
+ });
187
+ });
188
+
189
+ describe('fragment handling', () => {
190
+ it('should preserve inline fragments', () => {
191
+ const document = parse(`
192
+ query ProductList($options: ProductListOptions) {
193
+ products(options: $options) {
194
+ items {
195
+ id
196
+ name
197
+ ... on Product {
198
+ slug
199
+ description
200
+ }
201
+ }
202
+ totalItems
203
+ }
204
+ }
205
+ `);
206
+
207
+ const result = includeOnlySelectedListFields(document, [
208
+ { name: 'id', isCustomField: false },
209
+ { name: 'name', isCustomField: false },
210
+ ]);
211
+
212
+ const resultQuery = normalizeQuery(print(result));
213
+ expect(resultQuery).toContain('... on Product');
214
+ });
215
+
216
+ it('should preserve fragment spreads when they contain selected fields', () => {
217
+ const document = parse(`
218
+ query ProductList($options: ProductListOptions) {
219
+ products(options: $options) {
220
+ items {
221
+ id
222
+ ...ProductFields
223
+ }
224
+ totalItems
225
+ }
226
+ }
227
+
228
+ fragment ProductFields on Product {
229
+ name
230
+ slug
231
+ enabled
232
+ }
233
+ `);
234
+
235
+ const result = includeOnlySelectedListFields(document, [
236
+ { name: 'id', isCustomField: false },
237
+ { name: 'name', isCustomField: false },
238
+ ]);
239
+
240
+ const resultQuery = normalizeQuery(print(result));
241
+ expect(resultQuery).toContain('...ProductFields');
242
+ expect(resultQuery).toContain('fragment ProductFields');
243
+ expect(resultQuery).toContain(' name');
244
+ // Fragment should be filtered to only include selected fields
245
+ expect(resultQuery).not.toContain('slug');
246
+ expect(resultQuery).not.toContain('enabled');
247
+ });
248
+ });
249
+
250
+ describe('edge cases', () => {
251
+ it('should add id field if no fields selected to maintain valid query', () => {
252
+ const document = createTestDocument(`
253
+ id
254
+ name
255
+ slug
256
+ `);
257
+
258
+ // Select a field that doesn't exist in the document
259
+ const result = includeOnlySelectedListFields(document, [
260
+ { name: 'nonExistentField', isCustomField: false },
261
+ ]);
262
+
263
+ const resultQuery = normalizeQuery(print(result));
264
+ expect(resultQuery).toContain('items { id }');
265
+ expect(resultQuery).not.toContain('name');
266
+ expect(resultQuery).not.toContain('slug');
267
+ });
268
+
269
+ it('should handle document without items selectionSet', () => {
270
+ const document = parse(`
271
+ query ProductList($options: ProductListOptions) {
272
+ products(options: $options) {
273
+ items
274
+ totalItems
275
+ }
276
+ }
277
+ `);
278
+
279
+ const result = includeOnlySelectedListFields(document, [
280
+ { name: 'id', isCustomField: false },
281
+ { name: 'name', isCustomField: false },
282
+ ]);
283
+
284
+ const resultQuery = normalizeQuery(print(result));
285
+ expect(resultQuery).toContain('items');
286
+ expect(resultQuery).toContain('totalItems');
287
+ });
288
+
289
+ it('should handle multiple queries in document', () => {
290
+ const document = parse(`
291
+ query ProductList($options: ProductListOptions) {
292
+ products(options: $options) {
293
+ items {
294
+ id
295
+ name
296
+ slug
297
+ }
298
+ totalItems
299
+ }
300
+ }
301
+
302
+ query ProductCount {
303
+ products {
304
+ totalItems
305
+ }
306
+ }
307
+ `);
308
+
309
+ const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
310
+
311
+ // Should only modify the first query's items field
312
+ const resultQuery = normalizeQuery(print(result));
313
+ expect(resultQuery).toContain('query ProductList');
314
+ expect(resultQuery).toContain('query ProductCount');
315
+ });
316
+
317
+ it('should handle deeply nested structures', () => {
318
+ const document = createTestDocument(`
319
+ id
320
+ name
321
+ slug
322
+ variants {
323
+ id
324
+ name
325
+ options {
326
+ id
327
+ code
328
+ name
329
+ }
330
+ }
331
+ `);
332
+
333
+ const result = includeOnlySelectedListFields(document, [
334
+ { name: 'id', isCustomField: false },
335
+ { name: 'variants', isCustomField: false },
336
+ ]);
337
+
338
+ const resultQuery = normalizeQuery(print(result));
339
+ expect(resultQuery).toContain('variants');
340
+ expect(resultQuery).toContain('options');
341
+ // The nested name fields within variants are preserved, but root level name and slug should not be
342
+ expect(resultQuery).not.toContain('slug');
343
+ // Check that the structure is preserved correctly
344
+ expect(resultQuery).toContain('variants { id name options');
345
+ });
346
+
347
+ it('should preserve totalItems and other pagination fields', () => {
348
+ const document = parse(`
349
+ query ProductList($options: ProductListOptions) {
350
+ products(options: $options) {
351
+ items {
352
+ id
353
+ name
354
+ slug
355
+ }
356
+ totalItems
357
+ hasNextPage
358
+ hasPreviousPage
359
+ }
360
+ }
361
+ `);
362
+
363
+ const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
364
+
365
+ const resultQuery = normalizeQuery(print(result));
366
+ expect(resultQuery).toContain('totalItems');
367
+ expect(resultQuery).toContain('hasNextPage');
368
+ expect(resultQuery).toContain('hasPreviousPage');
369
+ expect(resultQuery).toContain('items { id }');
370
+ expect(resultQuery).not.toContain('name');
371
+ expect(resultQuery).not.toContain('slug');
372
+ });
373
+
374
+ it('should handle empty customFields selection gracefully', () => {
375
+ const document = createTestDocument(`
376
+ id
377
+ name
378
+ customFields {
379
+ field1
380
+ field2
381
+ }
382
+ `);
383
+
384
+ const result = includeOnlySelectedListFields(document, [
385
+ { name: 'id', isCustomField: false },
386
+ { name: 'nonExistentCustomField', isCustomField: true },
387
+ ]);
388
+
389
+ const resultQuery = normalizeQuery(print(result));
390
+ expect(resultQuery).toContain('items { id }');
391
+ expect(resultQuery).not.toContain('customFields');
392
+ expect(resultQuery).not.toContain('name');
393
+ });
394
+
395
+ it('should handle queries with aliases', () => {
396
+ const document = parse(`
397
+ query ProductList($options: ProductListOptions) {
398
+ allProducts: products(options: $options) {
399
+ items {
400
+ productId: id
401
+ productName: name
402
+ slug
403
+ }
404
+ totalItems
405
+ }
406
+ }
407
+ `);
408
+
409
+ const result = includeOnlySelectedListFields(document, [
410
+ { name: 'id', isCustomField: false },
411
+ { name: 'name', isCustomField: false },
412
+ ]);
413
+
414
+ const resultQuery = normalizeQuery(print(result));
415
+ // Note: aliases make this more complex - the function looks at field names
416
+ expect(resultQuery).toContain('productId: id');
417
+ expect(resultQuery).toContain('productName: name');
418
+ expect(resultQuery).not.toContain('slug');
419
+ });
420
+ });
421
+
422
+ describe('fragment-based items selection', () => {
423
+ it('should handle items defined in fragments - user example case', () => {
424
+ const document = parse(`
425
+ query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
426
+ facets(options: $options) {
427
+ items {
428
+ ...FacetWithValueList
429
+ }
430
+ totalItems
431
+ }
432
+ }
433
+
434
+ fragment FacetWithValueList on Facet {
435
+ id
436
+ createdAt
437
+ updatedAt
438
+ name
439
+ code
440
+ isPrivate
441
+ valueList(options: $facetValueListOptions) {
442
+ totalItems
443
+ items {
444
+ ...FacetValue
445
+ }
446
+ }
447
+ }
448
+
449
+ fragment FacetValue on FacetValue {
450
+ id
451
+ createdAt
452
+ updatedAt
453
+ languageCode
454
+ code
455
+ name
456
+ translations {
457
+ id
458
+ languageCode
459
+ name
460
+ }
461
+ facet {
462
+ id
463
+ createdAt
464
+ updatedAt
465
+ name
466
+ code
467
+ }
468
+ }
469
+ `);
470
+
471
+ const result = includeOnlySelectedListFields(document, [
472
+ { name: 'id', isCustomField: false },
473
+ { name: 'name', isCustomField: false },
474
+ { name: 'code', isCustomField: false },
475
+ ]);
476
+
477
+ const resultQuery = normalizeQuery(print(result));
478
+
479
+ // Should include selected fields
480
+ expect(resultQuery).toContain('...FacetWithValueList');
481
+ expect(resultQuery).toContain('fragment FacetWithValueList');
482
+
483
+ // Fragment should be filtered to only include selected fields
484
+ expect(resultQuery).toContain('id');
485
+ expect(resultQuery).toContain(' name');
486
+ expect(resultQuery).toContain(' code');
487
+
488
+ // Should exclude non-selected fields from fragment
489
+ expect(resultQuery).not.toContain('createdAt');
490
+ expect(resultQuery).not.toContain('updatedAt');
491
+ expect(resultQuery).not.toContain('isPrivate');
492
+ expect(resultQuery).not.toContain('valueList');
493
+
494
+ // Should remove unused FacetValue fragment
495
+ expect(resultQuery).not.toContain('fragment FacetValue');
496
+ expect(resultQuery).not.toContain('...FacetValue');
497
+ });
498
+
499
+ it('should handle nested fragments in items fragments', () => {
500
+ const document = parse(`
501
+ query ProductList($options: ProductListOptions) {
502
+ products(options: $options) {
503
+ items {
504
+ ...ProductWithAssets
505
+ }
506
+ totalItems
507
+ }
508
+ }
509
+
510
+ fragment ProductWithAssets on Product {
511
+ id
512
+ name
513
+ slug
514
+ featuredAsset {
515
+ ...AssetInfo
516
+ }
517
+ assets {
518
+ ...AssetInfo
519
+ }
520
+ }
521
+
522
+ fragment AssetInfo on Asset {
523
+ id
524
+ name
525
+ preview
526
+ source
527
+ width
528
+ height
529
+ }
530
+ `);
531
+
532
+ const result = includeOnlySelectedListFields(document, [
533
+ { name: 'name', isCustomField: false },
534
+ { name: 'featuredAsset', isCustomField: false },
535
+ ]);
536
+
537
+ const resultQuery = normalizeQuery(print(result));
538
+
539
+ // Should include selected fields
540
+ expect(resultQuery).toContain('...ProductWithAssets');
541
+ expect(resultQuery).toContain('fragment ProductWithAssets');
542
+
543
+ // Should include used nested fragments
544
+ expect(resultQuery).toContain('fragment AssetInfo');
545
+ expect(resultQuery).toContain('...AssetInfo');
546
+
547
+ // Fragment should be filtered
548
+ expect(resultQuery).toContain(' name');
549
+ expect(resultQuery).toContain('featuredAsset');
550
+
551
+ // Should exclude non-selected fields
552
+ expect(resultQuery).not.toContain('slug');
553
+ expect(resultQuery).not.toContain('assets');
554
+ });
555
+
556
+ it('should handle mixed direct fields and fragment spreads in items', () => {
557
+ const document = parse(`
558
+ query ProductList($options: ProductListOptions) {
559
+ products(options: $options) {
560
+ items {
561
+ id
562
+ enabled
563
+ ...ProductCore
564
+ customFields {
565
+ shortDescription
566
+ warrantyMonths
567
+ }
568
+ }
569
+ totalItems
570
+ }
571
+ }
572
+
573
+ fragment ProductCore on Product {
574
+ name
575
+ slug
576
+ description
577
+ featuredAsset {
578
+ id
579
+ preview
580
+ }
581
+ }
582
+ `);
583
+
584
+ const result = includeOnlySelectedListFields(document, [
585
+ { name: 'id', isCustomField: false },
586
+ { name: 'name', isCustomField: false },
587
+ { name: 'featuredAsset', isCustomField: false },
588
+ { name: 'shortDescription', isCustomField: true },
589
+ ]);
590
+
591
+ const resultQuery = normalizeQuery(print(result));
592
+
593
+ // Should include direct fields
594
+ expect(resultQuery).toContain('id');
595
+
596
+ // Should include fragment with filtered content
597
+ expect(resultQuery).toContain('...ProductCore');
598
+ expect(resultQuery).toContain('fragment ProductCore');
599
+ expect(resultQuery).toContain(' name');
600
+ expect(resultQuery).toContain('featuredAsset');
601
+
602
+ // Should include filtered custom fields
603
+ expect(resultQuery).toContain('customFields { shortDescription }');
604
+
605
+ // Should exclude non-selected fields
606
+ expect(resultQuery).not.toContain('enabled');
607
+ expect(resultQuery).not.toContain('slug');
608
+ expect(resultQuery).not.toContain('description');
609
+ expect(resultQuery).not.toContain('warrantyMonths');
610
+ });
611
+
612
+ it('should handle items with only fragment spreads', () => {
613
+ const document = parse(`
614
+ query CustomerList($options: CustomerListOptions) {
615
+ customers(options: $options) {
616
+ items {
617
+ ...CustomerBasic
618
+ ...CustomerContact
619
+ }
620
+ totalItems
621
+ }
622
+ }
623
+
624
+ fragment CustomerBasic on Customer {
625
+ id
626
+ title
627
+ firstName
628
+ lastName
629
+ emailAddress
630
+ }
631
+
632
+ fragment CustomerContact on Customer {
633
+ phoneNumber
634
+ addresses {
635
+ id
636
+ streetLine1
637
+ city
638
+ country {
639
+ code
640
+ name
641
+ }
642
+ }
643
+ }
644
+ `);
645
+
646
+ const result = includeOnlySelectedListFields(document, [
647
+ { name: 'firstName', isCustomField: false },
648
+ { name: 'lastName', isCustomField: false },
649
+ { name: 'emailAddress', isCustomField: false },
650
+ ]);
651
+
652
+ const resultQuery = normalizeQuery(print(result));
653
+
654
+ // Should include fragment with selected fields
655
+ expect(resultQuery).toContain('...CustomerBasic');
656
+ expect(resultQuery).toContain('fragment CustomerBasic');
657
+ expect(resultQuery).toContain('firstName');
658
+ expect(resultQuery).toContain('lastName');
659
+ expect(resultQuery).toContain('emailAddress');
660
+
661
+ // Should exclude unused fragment
662
+ expect(resultQuery).not.toContain('...CustomerContact');
663
+ expect(resultQuery).not.toContain('fragment CustomerContact');
664
+
665
+ // Should exclude non-selected fields from used fragment
666
+ expect(resultQuery).not.toContain('title');
667
+ expect(resultQuery).not.toContain('phoneNumber');
668
+ expect(resultQuery).not.toContain('addresses');
669
+ });
670
+
671
+ it('should handle deeply nested fragment spreads in items', () => {
672
+ const document = parse(`
673
+ query ProductList($options: ProductListOptions) {
674
+ products(options: $options) {
675
+ items {
676
+ ...ProductWithVariants
677
+ }
678
+ totalItems
679
+ }
680
+ }
681
+
682
+ fragment ProductWithVariants on Product {
683
+ id
684
+ name
685
+ variants {
686
+ id
687
+ name
688
+ options {
689
+ ...ProductOptionDetail
690
+ }
691
+ }
692
+ }
693
+
694
+ fragment ProductOptionDetail on ProductOption {
695
+ id
696
+ code
697
+ name
698
+ group {
699
+ id
700
+ name
701
+ code
702
+ }
703
+ }
704
+ `);
705
+
706
+ const result = includeOnlySelectedListFields(document, [
707
+ { name: 'name', isCustomField: false },
708
+ { name: 'variants', isCustomField: false },
709
+ ]);
710
+
711
+ const resultQuery = normalizeQuery(print(result));
712
+
713
+ // Should include main fragment
714
+ expect(resultQuery).toContain('...ProductWithVariants');
715
+ expect(resultQuery).toContain('fragment ProductWithVariants');
716
+
717
+ // Should include nested structures
718
+ expect(resultQuery).toContain(' name');
719
+ expect(resultQuery).toContain('variants');
720
+ expect(resultQuery).toContain('options');
721
+
722
+ // Should include nested fragment
723
+ expect(resultQuery).toContain('fragment ProductOptionDetail');
724
+ expect(resultQuery).toContain('...ProductOptionDetail');
725
+ });
726
+
727
+ it('should handle fragment spreads with custom fields in items', () => {
728
+ const document = parse(`
729
+ query ProductList($options: ProductListOptions) {
730
+ products(options: $options) {
731
+ items {
732
+ ...ProductWithCustomFields
733
+ }
734
+ totalItems
735
+ }
736
+ }
737
+
738
+ fragment ProductWithCustomFields on Product {
739
+ id
740
+ name
741
+ slug
742
+ customFields {
743
+ shortDescription
744
+ warrantyMonths
745
+ isEcoFriendly
746
+ featuredCollection {
747
+ id
748
+ name
749
+ }
750
+ }
751
+ }
752
+ `);
753
+
754
+ const result = includeOnlySelectedListFields(document, [
755
+ { name: 'name', isCustomField: false },
756
+ { name: 'shortDescription', isCustomField: true },
757
+ { name: 'isEcoFriendly', isCustomField: true },
758
+ ]);
759
+
760
+ const resultQuery = normalizeQuery(print(result));
761
+
762
+ // Should include fragment
763
+ expect(resultQuery).toContain('...ProductWithCustomFields');
764
+ expect(resultQuery).toContain('fragment ProductWithCustomFields');
765
+
766
+ // Should include selected regular field
767
+ expect(resultQuery).toContain(' name');
768
+
769
+ // Should include filtered custom fields
770
+ expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
771
+
772
+ // Should exclude non-selected fields
773
+ expect(resultQuery).not.toContain('slug');
774
+ expect(resultQuery).not.toContain('warrantyMonths');
775
+ expect(resultQuery).not.toContain('featuredCollection');
776
+ });
777
+
778
+ it('should only filter top-level items fragments, not nested fragments - user issue case', () => {
779
+ const document = parse(`
780
+ query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
781
+ facets(options: $options) {
782
+ items {
783
+ ...FacetWithValueList
784
+ }
785
+ totalItems
786
+ }
787
+ }
788
+
789
+ fragment FacetWithValueList on Facet {
790
+ id
791
+ name
792
+ isPrivate
793
+ valueList(options: $facetValueListOptions) {
794
+ totalItems
795
+ items {
796
+ ...FacetValue
797
+ }
798
+ }
799
+ }
800
+
801
+ fragment FacetValue on FacetValue {
802
+ id
803
+ name
804
+ code
805
+ translations {
806
+ name
807
+ languageCode
808
+ }
809
+ }
810
+ `);
811
+
812
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
813
+
814
+ const resultQuery = normalizeQuery(print(result));
815
+
816
+ // Should include the top-level fragment
817
+ expect(resultQuery).toContain('...FacetWithValueList');
818
+ expect(resultQuery).toContain('fragment FacetWithValueList');
819
+
820
+ // Top-level fragment should be filtered (only name, no isPrivate)
821
+ expect(resultQuery).toContain(' name');
822
+ expect(resultQuery).not.toContain('isPrivate');
823
+ expect(resultQuery).not.toContain('valueList'); // This should be filtered out
824
+
825
+ // Should NOT include the nested FacetValue fragment since valueList was removed
826
+ expect(resultQuery).not.toContain('fragment FacetValue');
827
+ expect(resultQuery).not.toContain('...FacetValue');
828
+ });
829
+
830
+ it('should preserve nested fragments when their parent field is selected', () => {
831
+ const document = parse(`
832
+ query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
833
+ facets(options: $options) {
834
+ items {
835
+ ...FacetWithValueList
836
+ }
837
+ totalItems
838
+ }
839
+ }
840
+
841
+ fragment FacetWithValueList on Facet {
842
+ id
843
+ name
844
+ isPrivate
845
+ valueList(options: $facetValueListOptions) {
846
+ totalItems
847
+ items {
848
+ ...FacetValue
849
+ }
850
+ }
851
+ }
852
+
853
+ fragment FacetValue on FacetValue {
854
+ id
855
+ name
856
+ code
857
+ translations {
858
+ name
859
+ languageCode
860
+ }
861
+ }
862
+ `);
863
+
864
+ const result = includeOnlySelectedListFields(document, [
865
+ { name: 'name', isCustomField: false },
866
+ { name: 'valueList', isCustomField: false },
867
+ ]);
868
+
869
+ const resultQuery = normalizeQuery(print(result));
870
+
871
+ // Should include the top-level fragment
872
+ expect(resultQuery).toContain('...FacetWithValueList');
873
+ expect(resultQuery).toContain('fragment FacetWithValueList');
874
+
875
+ // Top-level fragment should be filtered to include name and valueList
876
+ expect(resultQuery).toContain(' name');
877
+ expect(resultQuery).toContain('valueList');
878
+ expect(resultQuery).not.toContain('isPrivate');
879
+
880
+ // Should include the nested FacetValue fragment UNCHANGED since it's not a top-level items fragment
881
+ expect(resultQuery).toContain('fragment FacetValue');
882
+ expect(resultQuery).toContain('...FacetValue');
883
+ expect(resultQuery).toContain(' code'); // This should be preserved in the nested fragment
884
+ expect(resultQuery).toContain('translations'); // This should be preserved in the nested fragment
885
+ });
886
+ });
887
+
888
+ describe('unused fragment removal', () => {
889
+ it('should remove unused fragments when fields using them are filtered out', () => {
890
+ const document = parse(`
891
+ query ProductVariantList($options: ProductVariantListOptions) {
892
+ productVariants(options: $options) {
893
+ items {
894
+ id
895
+ price
896
+ priceWithTax
897
+ featuredAsset {
898
+ ...Asset
899
+ }
900
+ }
901
+ totalItems
902
+ }
903
+ }
904
+
905
+ fragment Asset on Asset {
906
+ id
907
+ createdAt
908
+ updatedAt
909
+ name
910
+ fileSize
911
+ mimeType
912
+ type
913
+ preview
914
+ source
915
+ width
916
+ height
917
+ focalPoint {
918
+ x
919
+ y
920
+ }
921
+ }
922
+ `);
923
+
924
+ // Select only price fields, excluding featuredAsset
925
+ const result = includeOnlySelectedListFields(document, [
926
+ { name: 'price', isCustomField: false },
927
+ { name: 'priceWithTax', isCustomField: false },
928
+ ]);
929
+
930
+ const resultQuery = normalizeQuery(print(result));
931
+
932
+ // Should include selected fields
933
+ expect(resultQuery).toContain('price');
934
+ expect(resultQuery).toContain('priceWithTax');
935
+
936
+ // Should exclude featuredAsset field
937
+ expect(resultQuery).not.toContain('featuredAsset');
938
+
939
+ // Should remove unused Asset fragment
940
+ expect(resultQuery).not.toContain('fragment Asset');
941
+ expect(resultQuery).not.toContain('...Asset');
942
+ });
943
+
944
+ it('should keep used fragments when fields using them are selected', () => {
945
+ const document = parse(`
946
+ query ProductVariantList($options: ProductVariantListOptions) {
947
+ productVariants(options: $options) {
948
+ items {
949
+ id
950
+ price
951
+ featuredAsset {
952
+ ...Asset
953
+ }
954
+ }
955
+ totalItems
956
+ }
957
+ }
958
+
959
+ fragment Asset on Asset {
960
+ id
961
+ name
962
+ preview
963
+ source
964
+ }
965
+ `);
966
+
967
+ // Select fields including featuredAsset
968
+ const result = includeOnlySelectedListFields(document, [
969
+ { name: 'price', isCustomField: false },
970
+ { name: 'featuredAsset', isCustomField: false },
971
+ ]);
972
+
973
+ const resultQuery = normalizeQuery(print(result));
974
+
975
+ // Should include selected fields
976
+ expect(resultQuery).toContain('price');
977
+ expect(resultQuery).toContain('featuredAsset');
978
+
979
+ // Should keep used Asset fragment
980
+ expect(resultQuery).toContain('fragment Asset');
981
+ expect(resultQuery).toContain('...Asset');
982
+ });
983
+
984
+ it('should handle nested fragment dependencies', () => {
985
+ const document = parse(`
986
+ query ProductList($options: ProductListOptions) {
987
+ products(options: $options) {
988
+ items {
989
+ id
990
+ name
991
+ featuredAsset {
992
+ ...AssetWithMetadata
993
+ }
994
+ }
995
+ totalItems
996
+ }
997
+ }
998
+
999
+ fragment AssetWithMetadata on Asset {
1000
+ ...AssetCore
1001
+ metadata {
1002
+ key
1003
+ value
1004
+ }
1005
+ }
1006
+
1007
+ fragment AssetCore on Asset {
1008
+ id
1009
+ name
1010
+ preview
1011
+ source
1012
+ }
1013
+ `);
1014
+
1015
+ // Select only name, excluding featuredAsset
1016
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
1017
+
1018
+ const resultQuery = normalizeQuery(print(result));
1019
+
1020
+ // Should include selected field
1021
+ expect(resultQuery).toContain(' name');
1022
+
1023
+ // Should exclude featuredAsset field
1024
+ expect(resultQuery).not.toContain('featuredAsset');
1025
+
1026
+ // Should remove all unused fragments
1027
+ expect(resultQuery).not.toContain('fragment AssetWithMetadata');
1028
+ expect(resultQuery).not.toContain('fragment AssetCore');
1029
+ expect(resultQuery).not.toContain('...AssetWithMetadata');
1030
+ expect(resultQuery).not.toContain('...AssetCore');
1031
+ });
1032
+
1033
+ it('should keep transitive fragment dependencies when parent fragment is used', () => {
1034
+ const document = parse(`
1035
+ query ProductList($options: ProductListOptions) {
1036
+ products(options: $options) {
1037
+ items {
1038
+ id
1039
+ name
1040
+ featuredAsset {
1041
+ ...AssetWithMetadata
1042
+ }
1043
+ }
1044
+ totalItems
1045
+ }
1046
+ }
1047
+
1048
+ fragment AssetWithMetadata on Asset {
1049
+ ...AssetCore
1050
+ metadata {
1051
+ key
1052
+ value
1053
+ }
1054
+ }
1055
+
1056
+ fragment AssetCore on Asset {
1057
+ id
1058
+ name
1059
+ preview
1060
+ source
1061
+ }
1062
+ `);
1063
+
1064
+ // Select featuredAsset, which should keep all related fragments
1065
+ const result = includeOnlySelectedListFields(document, [
1066
+ { name: 'name', isCustomField: false },
1067
+ { name: 'featuredAsset', isCustomField: false },
1068
+ ]);
1069
+
1070
+ const resultQuery = normalizeQuery(print(result));
1071
+
1072
+ // Should include selected fields
1073
+ expect(resultQuery).toContain(' name');
1074
+ expect(resultQuery).toContain('featuredAsset');
1075
+
1076
+ // Should keep all used fragments
1077
+ expect(resultQuery).toContain('fragment AssetWithMetadata');
1078
+ expect(resultQuery).toContain('fragment AssetCore');
1079
+ expect(resultQuery).toContain('...AssetWithMetadata');
1080
+ expect(resultQuery).toContain('...AssetCore');
1081
+ });
1082
+
1083
+ it('should handle mixed used and unused fragments', () => {
1084
+ const document = parse(`
1085
+ query ProductList($options: ProductListOptions) {
1086
+ products(options: $options) {
1087
+ items {
1088
+ id
1089
+ name
1090
+ featuredAsset {
1091
+ ...Asset
1092
+ }
1093
+ variants {
1094
+ ...ProductVariant
1095
+ }
1096
+ }
1097
+ totalItems
1098
+ }
1099
+ }
1100
+
1101
+ fragment Asset on Asset {
1102
+ id
1103
+ name
1104
+ preview
1105
+ }
1106
+
1107
+ fragment ProductVariant on ProductVariant {
1108
+ id
1109
+ name
1110
+ sku
1111
+ price
1112
+ }
1113
+
1114
+ fragment UnusedFragment on Product {
1115
+ description
1116
+ slug
1117
+ }
1118
+ `);
1119
+
1120
+ // Select name and featuredAsset, excluding variants
1121
+ const result = includeOnlySelectedListFields(document, [
1122
+ { name: 'name', isCustomField: false },
1123
+ { name: 'featuredAsset', isCustomField: false },
1124
+ ]);
1125
+
1126
+ const resultQuery = normalizeQuery(print(result));
1127
+
1128
+ // Should include selected fields
1129
+ expect(resultQuery).toContain(' name');
1130
+ expect(resultQuery).toContain('featuredAsset');
1131
+
1132
+ // Should keep used Asset fragment
1133
+ expect(resultQuery).toContain('fragment Asset');
1134
+ expect(resultQuery).toContain('...Asset');
1135
+
1136
+ // Should exclude variants field
1137
+ expect(resultQuery).not.toContain('variants');
1138
+
1139
+ // Should remove unused fragments
1140
+ expect(resultQuery).not.toContain('fragment ProductVariant');
1141
+ expect(resultQuery).not.toContain('fragment UnusedFragment');
1142
+ expect(resultQuery).not.toContain('...ProductVariant');
1143
+ });
1144
+
1145
+ it('should handle the exact case from user example', () => {
1146
+ const document = parse(`
1147
+ query ProductVariantList($options: ProductVariantListOptions) {
1148
+ productVariants(options: $options) {
1149
+ items {
1150
+ featuredAsset {
1151
+ ...Asset
1152
+ }
1153
+ price
1154
+ priceWithTax
1155
+ stockLevels {
1156
+ id
1157
+ stockOnHand
1158
+ stockAllocated
1159
+ }
1160
+ }
1161
+ totalItems
1162
+ }
1163
+ }
1164
+
1165
+ fragment Asset on Asset {
1166
+ id
1167
+ createdAt
1168
+ updatedAt
1169
+ name
1170
+ fileSize
1171
+ mimeType
1172
+ type
1173
+ preview
1174
+ source
1175
+ width
1176
+ height
1177
+ focalPoint {
1178
+ x
1179
+ y
1180
+ }
1181
+ }
1182
+ `);
1183
+
1184
+ // Remove featuredAsset field as mentioned in the user's example
1185
+ const result = includeOnlySelectedListFields(document, [
1186
+ { name: 'price', isCustomField: false },
1187
+ { name: 'priceWithTax', isCustomField: false },
1188
+ { name: 'stockLevels', isCustomField: false },
1189
+ ]);
1190
+
1191
+ const resultQuery = normalizeQuery(print(result));
1192
+
1193
+ // Should include selected fields
1194
+ expect(resultQuery).toContain('price');
1195
+ expect(resultQuery).toContain('priceWithTax');
1196
+ expect(resultQuery).toContain('stockLevels');
1197
+
1198
+ // Should exclude featuredAsset
1199
+ expect(resultQuery).not.toContain('featuredAsset');
1200
+
1201
+ // Should remove unused Asset fragment to prevent GraphQL error
1202
+ expect(resultQuery).not.toContain('fragment Asset');
1203
+ expect(resultQuery).not.toContain('...Asset');
1204
+
1205
+ // Verify the document is valid GraphQL by checking basic structure
1206
+ expect(resultQuery).toContain('query ProductVariantList');
1207
+ expect(resultQuery).toContain('productVariants');
1208
+ expect(resultQuery).toContain('items');
1209
+ expect(resultQuery).toContain('totalItems');
1210
+ });
1211
+ });
1212
+
1213
+ describe('unused variable removal', () => {
1214
+ it('should remove unused variables when fields using them are filtered out - user issue case', () => {
1215
+ const document = parse(`
1216
+ query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
1217
+ facets(options: $options) {
1218
+ items {
1219
+ ...FacetWithValueList
1220
+ }
1221
+ totalItems
1222
+ }
1223
+ }
1224
+
1225
+ fragment FacetWithValueList on Facet {
1226
+ id
1227
+ name
1228
+ isPrivate
1229
+ valueList(options: $facetValueListOptions) {
1230
+ totalItems
1231
+ items {
1232
+ ...FacetValue
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ fragment FacetValue on FacetValue {
1238
+ name
1239
+ }
1240
+ `);
1241
+
1242
+ // Select only name, which should filter out valueList and its variable
1243
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
1244
+
1245
+ const resultQuery = normalizeQuery(print(result));
1246
+
1247
+ // Should include selected field
1248
+ expect(resultQuery).toContain(' name');
1249
+
1250
+ // Should exclude valueList field
1251
+ expect(resultQuery).not.toContain('valueList');
1252
+
1253
+ // Should remove unused $facetValueListOptions variable
1254
+ expect(resultQuery).not.toContain('$facetValueListOptions');
1255
+ expect(resultQuery).not.toContain('FacetValueListOptions');
1256
+
1257
+ // Should keep used $options variable
1258
+ expect(resultQuery).toContain('$options');
1259
+ expect(resultQuery).toContain('FacetListOptions');
1260
+
1261
+ // Should remove unused FacetValue fragment
1262
+ expect(resultQuery).not.toContain('fragment FacetValue');
1263
+ });
1264
+
1265
+ it('should preserve variables that are still used', () => {
1266
+ const document = parse(`
1267
+ query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
1268
+ facets(options: $options) {
1269
+ items {
1270
+ ...FacetWithValueList
1271
+ }
1272
+ totalItems
1273
+ }
1274
+ }
1275
+
1276
+ fragment FacetWithValueList on Facet {
1277
+ id
1278
+ name
1279
+ valueList(options: $facetValueListOptions) {
1280
+ totalItems
1281
+ items {
1282
+ ...FacetValue
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ fragment FacetValue on FacetValue {
1288
+ name
1289
+ }
1290
+ `);
1291
+
1292
+ // Select name and valueList, which should preserve the variable
1293
+ const result = includeOnlySelectedListFields(document, [
1294
+ { name: 'name', isCustomField: false },
1295
+ { name: 'valueList', isCustomField: false },
1296
+ ]);
1297
+
1298
+ const resultQuery = normalizeQuery(print(result));
1299
+
1300
+ // Should include selected fields
1301
+ expect(resultQuery).toContain(' name');
1302
+ expect(resultQuery).toContain('valueList');
1303
+
1304
+ // Should keep both variables since both are used
1305
+ expect(resultQuery).toContain('$options');
1306
+ expect(resultQuery).toContain('$facetValueListOptions');
1307
+ expect(resultQuery).toContain('FacetListOptions');
1308
+ expect(resultQuery).toContain('FacetValueListOptions');
1309
+
1310
+ // Should keep FacetValue fragment since valueList is preserved
1311
+ expect(resultQuery).toContain('fragment FacetValue');
1312
+ });
1313
+
1314
+ it('should handle multiple variables with complex usage patterns', () => {
1315
+ const document = parse(`
1316
+ query ComplexQuery($listOptions: ListOptions, $searchTerm: String, $categoryId: ID, $priceRange: PriceRangeInput) {
1317
+ products(options: $listOptions) {
1318
+ items {
1319
+ id
1320
+ name
1321
+ searchResults(term: $searchTerm) {
1322
+ score
1323
+ highlight
1324
+ }
1325
+ category(id: $categoryId) {
1326
+ name
1327
+ path
1328
+ }
1329
+ variants(priceRange: $priceRange) {
1330
+ price
1331
+ priceWithTax
1332
+ }
1333
+ }
1334
+ totalItems
1335
+ }
1336
+ }
1337
+ `);
1338
+
1339
+ // Select only id and name, should remove most variables
1340
+ const result = includeOnlySelectedListFields(document, [
1341
+ { name: 'id', isCustomField: false },
1342
+ { name: 'name', isCustomField: false },
1343
+ ]);
1344
+
1345
+ const resultQuery = normalizeQuery(print(result));
1346
+
1347
+ // Should include selected fields
1348
+ expect(resultQuery).toContain('id');
1349
+ expect(resultQuery).toContain(' name');
1350
+
1351
+ // Should exclude fields that use variables
1352
+ expect(resultQuery).not.toContain('searchResults');
1353
+ expect(resultQuery).not.toContain('category');
1354
+ expect(resultQuery).not.toContain('variants');
1355
+
1356
+ // Should keep only the used variable
1357
+ expect(resultQuery).toContain('$listOptions');
1358
+ expect(resultQuery).toContain('ListOptions');
1359
+
1360
+ // Should remove unused variables
1361
+ expect(resultQuery).not.toContain('$searchTerm');
1362
+ expect(resultQuery).not.toContain('$categoryId');
1363
+ expect(resultQuery).not.toContain('$priceRange');
1364
+ expect(resultQuery).not.toContain('String');
1365
+ expect(resultQuery).not.toContain('ID');
1366
+ expect(resultQuery).not.toContain('PriceRangeInput');
1367
+ });
1368
+
1369
+ it('should handle variables in query-level arguments', () => {
1370
+ const document = parse(`
1371
+ query ProductSearch($options: ProductListOptions, $filters: ProductFilterInput) {
1372
+ products(options: $options, filters: $filters) {
1373
+ items {
1374
+ id
1375
+ name
1376
+ price
1377
+ category {
1378
+ name
1379
+ }
1380
+ }
1381
+ totalItems
1382
+ }
1383
+ }
1384
+ `);
1385
+
1386
+ // Select only name, which should remove fields but keep variables used at query level
1387
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
1388
+
1389
+ const resultQuery = normalizeQuery(print(result));
1390
+
1391
+ // Should include selected field
1392
+ expect(resultQuery).toContain(' name');
1393
+
1394
+ // Should exclude non-selected fields
1395
+ expect(resultQuery).not.toContain('price');
1396
+ expect(resultQuery).not.toContain('category');
1397
+
1398
+ // Should keep both variables since they're used at query level
1399
+ expect(resultQuery).toContain('$options');
1400
+ expect(resultQuery).toContain('$filters');
1401
+ expect(resultQuery).toContain('ProductListOptions');
1402
+ expect(resultQuery).toContain('ProductFilterInput');
1403
+ });
1404
+
1405
+ it('should handle variables used in fragment arguments', () => {
1406
+ const document = parse(`
1407
+ query ProductList($options: ProductListOptions, $assetOptions: AssetListOptions) {
1408
+ products(options: $options) {
1409
+ items {
1410
+ ...ProductWithAssets
1411
+ }
1412
+ totalItems
1413
+ }
1414
+ }
1415
+
1416
+ fragment ProductWithAssets on Product {
1417
+ id
1418
+ name
1419
+ assets(options: $assetOptions) {
1420
+ id
1421
+ preview
1422
+ }
1423
+ }
1424
+ `);
1425
+
1426
+ // Select only name, which should remove assets field and its variable
1427
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
1428
+
1429
+ const resultQuery = normalizeQuery(print(result));
1430
+
1431
+ // Should include selected field
1432
+ expect(resultQuery).toContain(' name');
1433
+
1434
+ // Should exclude assets field
1435
+ expect(resultQuery).not.toContain('assets');
1436
+
1437
+ // Should keep used variable
1438
+ expect(resultQuery).toContain('$options');
1439
+ expect(resultQuery).toContain('ProductListOptions');
1440
+
1441
+ // Should remove unused variable
1442
+ expect(resultQuery).not.toContain('$assetOptions');
1443
+ expect(resultQuery).not.toContain('AssetListOptions');
1444
+ });
1445
+
1446
+ it('should handle edge case with no variables', () => {
1447
+ const document = parse(`
1448
+ query ProductList {
1449
+ products {
1450
+ items {
1451
+ id
1452
+ name
1453
+ slug
1454
+ }
1455
+ totalItems
1456
+ }
1457
+ }
1458
+ `);
1459
+
1460
+ const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
1461
+
1462
+ const resultQuery = normalizeQuery(print(result));
1463
+
1464
+ // Should include selected field
1465
+ expect(resultQuery).toContain(' name');
1466
+
1467
+ // Should exclude non-selected fields
1468
+ expect(resultQuery).not.toContain('slug');
1469
+
1470
+ // Query should remain valid with no variables
1471
+ expect(resultQuery).toContain('query ProductList {');
1472
+ });
1473
+ });
1474
+
1475
+ describe('complex real-world scenarios', () => {
1476
+ it('should handle a complex product list query', () => {
1477
+ const document = parse(`
1478
+ query GetProducts($options: ProductListOptions) {
1479
+ products(options: $options) {
1480
+ items {
1481
+ id
1482
+ createdAt
1483
+ updatedAt
1484
+ name
1485
+ slug
1486
+ enabled
1487
+ featuredAsset {
1488
+ id
1489
+ preview
1490
+ source
1491
+ width
1492
+ height
1493
+ }
1494
+ variantList {
1495
+ totalItems
1496
+ items {
1497
+ id
1498
+ name
1499
+ sku
1500
+ }
1501
+ }
1502
+ customFields {
1503
+ shortDescription
1504
+ warrantyMonths
1505
+ isEcoFriendly
1506
+ featuredCollection {
1507
+ id
1508
+ name
1509
+ }
1510
+ }
1511
+ }
1512
+ totalItems
1513
+ }
1514
+ }
1515
+ `);
1516
+
1517
+ const result = includeOnlySelectedListFields(document, [
1518
+ { name: 'id', isCustomField: false },
1519
+ { name: 'name', isCustomField: false },
1520
+ { name: 'featuredAsset', isCustomField: false },
1521
+ { name: 'shortDescription', isCustomField: true },
1522
+ { name: 'isEcoFriendly', isCustomField: true },
1523
+ ]);
1524
+
1525
+ const resultQuery = normalizeQuery(print(result));
1526
+
1527
+ // Should include selected fields
1528
+ expect(resultQuery).toContain('id');
1529
+ expect(resultQuery).toContain(' name');
1530
+ expect(resultQuery).toContain('featuredAsset');
1531
+ expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
1532
+
1533
+ // Should exclude non-selected fields
1534
+ expect(resultQuery).not.toContain('createdAt');
1535
+ expect(resultQuery).not.toContain('updatedAt');
1536
+ expect(resultQuery).not.toContain('slug');
1537
+ expect(resultQuery).not.toContain('enabled');
1538
+ expect(resultQuery).not.toContain('variantList');
1539
+ expect(resultQuery).not.toContain('warrantyMonths');
1540
+ expect(resultQuery).not.toContain('featuredCollection');
1541
+
1542
+ // Should preserve pagination
1543
+ expect(resultQuery).toContain('totalItems');
1544
+ });
1545
+
1546
+ it('should handle customer list with addresses', () => {
1547
+ const document = parse(`
1548
+ query CustomerList($options: CustomerListOptions) {
1549
+ customers(options: $options) {
1550
+ items {
1551
+ id
1552
+ title
1553
+ firstName
1554
+ lastName
1555
+ emailAddress
1556
+ phoneNumber
1557
+ addresses {
1558
+ id
1559
+ streetLine1
1560
+ streetLine2
1561
+ city
1562
+ province
1563
+ postalCode
1564
+ country {
1565
+ id
1566
+ code
1567
+ name
1568
+ }
1569
+ }
1570
+ orders {
1571
+ totalItems
1572
+ }
1573
+ }
1574
+ totalItems
1575
+ }
1576
+ }
1577
+ `);
1578
+
1579
+ const result = includeOnlySelectedListFields(document, [
1580
+ { name: 'id', isCustomField: false },
1581
+ { name: 'emailAddress', isCustomField: false },
1582
+ { name: 'firstName', isCustomField: false },
1583
+ { name: 'lastName', isCustomField: false },
1584
+ ]);
1585
+
1586
+ const resultQuery = normalizeQuery(print(result));
1587
+
1588
+ // Should include selected fields
1589
+ expect(resultQuery).toContain('id');
1590
+ expect(resultQuery).toContain('emailAddress');
1591
+ expect(resultQuery).toContain('firstName');
1592
+ expect(resultQuery).toContain('lastName');
1593
+
1594
+ // Should exclude non-selected fields
1595
+ expect(resultQuery).not.toContain('title');
1596
+ expect(resultQuery).not.toContain('phoneNumber');
1597
+ expect(resultQuery).not.toContain('addresses');
1598
+ expect(resultQuery).not.toContain('orders');
1599
+ });
1600
+ });
1601
+
1602
+ describe('memoization', () => {
1603
+ it('should return the same reference for identical inputs', () => {
1604
+ const document = parse(`
1605
+ query ProductList($options: ProductListOptions) {
1606
+ products(options: $options) {
1607
+ items {
1608
+ id
1609
+ name
1610
+ slug
1611
+ enabled
1612
+ }
1613
+ totalItems
1614
+ }
1615
+ }
1616
+ `);
1617
+
1618
+ const selectedColumns = [
1619
+ { name: 'id', isCustomField: false },
1620
+ { name: 'name', isCustomField: false },
1621
+ ];
1622
+
1623
+ const result1 = includeOnlySelectedListFields(document, selectedColumns);
1624
+ const result2 = includeOnlySelectedListFields(document, selectedColumns);
1625
+
1626
+ // Should return the exact same reference from cache
1627
+ expect(result1).toBe(result2);
1628
+ });
1629
+
1630
+ it('should handle different column orders consistently', () => {
1631
+ const document = parse(`
1632
+ query ProductList($options: ProductListOptions) {
1633
+ products(options: $options) {
1634
+ items {
1635
+ id
1636
+ name
1637
+ slug
1638
+ }
1639
+ totalItems
1640
+ }
1641
+ }
1642
+ `);
1643
+
1644
+ const columns1 = [
1645
+ { name: 'name', isCustomField: false },
1646
+ { name: 'id', isCustomField: false },
1647
+ ];
1648
+
1649
+ const columns2 = [
1650
+ { name: 'id', isCustomField: false },
1651
+ { name: 'name', isCustomField: false },
1652
+ ];
1653
+
1654
+ const result1 = includeOnlySelectedListFields(document, columns1);
1655
+ const result2 = includeOnlySelectedListFields(document, columns2);
1656
+
1657
+ // Should return the same result regardless of column order (due to sorting in cache key)
1658
+ expect(result1).toBe(result2);
1659
+ });
1660
+ });
1661
+
1662
+ describe('column dependencies', () => {
1663
+ it('should include dependency fields even when not explicitly selected', () => {
1664
+ const document = parse(`
1665
+ query CollectionList($options: CollectionListOptions) {
1666
+ collections(options: $options) {
1667
+ items {
1668
+ id
1669
+ name
1670
+ slug
1671
+ children {
1672
+ id
1673
+ name
1674
+ }
1675
+ breadcrumbs {
1676
+ id
1677
+ name
1678
+ }
1679
+ position
1680
+ }
1681
+ totalItems
1682
+ }
1683
+ }
1684
+ `);
1685
+
1686
+ // Select only 'name' but declare dependencies on 'children' and 'breadcrumbs'
1687
+ const result = includeOnlySelectedListFields(document, [
1688
+ {
1689
+ name: 'name',
1690
+ isCustomField: false,
1691
+ dependencies: ['children', 'breadcrumbs'],
1692
+ },
1693
+ ]);
1694
+
1695
+ const resultQuery = normalizeQuery(print(result));
1696
+
1697
+ // Should include selected field
1698
+ expect(resultQuery).toContain('name');
1699
+
1700
+ // Should include dependency fields
1701
+ expect(resultQuery).toContain('children');
1702
+ expect(resultQuery).toContain('breadcrumbs');
1703
+
1704
+ // Should exclude non-selected, non-dependency fields
1705
+ expect(resultQuery).not.toContain('slug');
1706
+ expect(resultQuery).not.toContain('position');
1707
+ });
1708
+
1709
+ it('should handle multiple columns with different dependencies', () => {
1710
+ const document = parse(`
1711
+ query ProductList($options: ProductListOptions) {
1712
+ products(options: $options) {
1713
+ items {
1714
+ id
1715
+ name
1716
+ slug
1717
+ enabled
1718
+ variants {
1719
+ id
1720
+ name
1721
+ }
1722
+ assets {
1723
+ id
1724
+ preview
1725
+ }
1726
+ featuredAsset {
1727
+ id
1728
+ preview
1729
+ }
1730
+ }
1731
+ totalItems
1732
+ }
1733
+ }
1734
+ `);
1735
+
1736
+ const result = includeOnlySelectedListFields(document, [
1737
+ {
1738
+ name: 'name',
1739
+ isCustomField: false,
1740
+ dependencies: ['featuredAsset'], // For thumbnail display
1741
+ },
1742
+ {
1743
+ name: 'enabled',
1744
+ isCustomField: false,
1745
+ dependencies: ['variants'], // For variant count display
1746
+ },
1747
+ ]);
1748
+
1749
+ const resultQuery = normalizeQuery(print(result));
1750
+
1751
+ // Should include selected fields
1752
+ expect(resultQuery).toContain('name');
1753
+ expect(resultQuery).toContain('enabled');
1754
+
1755
+ // Should include dependency fields
1756
+ expect(resultQuery).toContain('featuredAsset');
1757
+ expect(resultQuery).toContain('variants');
1758
+
1759
+ // Should exclude non-selected, non-dependency fields
1760
+ expect(resultQuery).not.toContain('slug');
1761
+ expect(resultQuery).not.toContain('assets');
1762
+ });
1763
+
1764
+ it('should cache correctly with dependencies', () => {
1765
+ const document = parse(`
1766
+ query TestList($options: TestListOptions) {
1767
+ tests(options: $options) {
1768
+ items {
1769
+ id
1770
+ name
1771
+ data
1772
+ metadata
1773
+ }
1774
+ totalItems
1775
+ }
1776
+ }
1777
+ `);
1778
+
1779
+ const columnsWithDeps = [
1780
+ {
1781
+ name: 'name',
1782
+ isCustomField: false,
1783
+ dependencies: ['metadata'],
1784
+ },
1785
+ ];
1786
+
1787
+ const result1 = includeOnlySelectedListFields(document, columnsWithDeps);
1788
+ const result2 = includeOnlySelectedListFields(document, columnsWithDeps);
1789
+
1790
+ // Should return the same reference from cache
1791
+ expect(result1).toBe(result2);
1792
+ });
1793
+
1794
+ it('should handle dependencies with custom fields', () => {
1795
+ const document = parse(`
1796
+ query ProductList($options: ProductListOptions) {
1797
+ products(options: $options) {
1798
+ items {
1799
+ id
1800
+ name
1801
+ customFields {
1802
+ shortDescription
1803
+ isEcoFriendly
1804
+ relatedProducts {
1805
+ id
1806
+ name
1807
+ }
1808
+ }
1809
+ }
1810
+ totalItems
1811
+ }
1812
+ }
1813
+ `);
1814
+
1815
+ const result = includeOnlySelectedListFields(document, [
1816
+ {
1817
+ name: 'name',
1818
+ isCustomField: false,
1819
+ dependencies: ['customFields'], // Depends on custom fields for rendering logic
1820
+ },
1821
+ {
1822
+ name: 'shortDescription',
1823
+ isCustomField: true,
1824
+ },
1825
+ ]);
1826
+
1827
+ const resultQuery = normalizeQuery(print(result));
1828
+
1829
+ // Should include selected fields
1830
+ expect(resultQuery).toContain('name');
1831
+
1832
+ // Should include customFields (as dependency and for custom field)
1833
+ expect(resultQuery).toContain('customFields');
1834
+ expect(resultQuery).toContain('shortDescription');
1835
+
1836
+ // Should include id for valid query
1837
+ expect(resultQuery).toContain('id');
1838
+ });
1839
+ });
1840
+ });