@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,940 @@
1
+ import {
2
+ ArgumentNode,
3
+ DocumentNode,
4
+ FieldNode,
5
+ FragmentDefinitionNode,
6
+ FragmentSpreadNode,
7
+ Kind,
8
+ SelectionNode,
9
+ VariableNode,
10
+ visit,
11
+ } from 'graphql';
12
+
13
+ import { getQueryName } from './get-document-structure.js';
14
+
15
+ // Simple LRU-style cache for memoization
16
+ const filterCache = new Map<string, DocumentNode>();
17
+ const MAX_CACHE_SIZE = 100;
18
+
19
+ // Fast document fingerprinting using WeakMap for reference tracking
20
+ const documentIds = new WeakMap<DocumentNode, string>();
21
+ let documentCounter = 0;
22
+
23
+ /**
24
+ * Get a fast, stable ID for a document node
25
+ */
26
+ function getDocumentId(document: DocumentNode): string {
27
+ let id = documentIds.get(document);
28
+ if (!id) {
29
+ // For new documents, create a lightweight structural hash
30
+ id = createDocumentFingerprint(document);
31
+ documentIds.set(document, id);
32
+ }
33
+ return id;
34
+ }
35
+
36
+ /**
37
+ * Create a lightweight fingerprint of document structure (much faster than print())
38
+ */
39
+ function createDocumentFingerprint(document: DocumentNode): string {
40
+ const parts: string[] = [];
41
+
42
+ for (const def of document.definitions) {
43
+ if (def.kind === Kind.OPERATION_DEFINITION) {
44
+ parts.push(`op:${def.operation}:${def.name?.value || 'anon'}`);
45
+ // Just count selections, don't traverse them
46
+ parts.push(`sel:${def.selectionSet.selections.length}`);
47
+ } else if (def.kind === Kind.FRAGMENT_DEFINITION) {
48
+ parts.push(`frag:${def.name.value}:${def.typeCondition.name.value}`);
49
+ parts.push(`sel:${def.selectionSet.selections.length}`);
50
+ }
51
+ }
52
+
53
+ return `doc_${++documentCounter}_${parts.join('_')}`;
54
+ }
55
+
56
+ /**
57
+ * Create a stable cache key from document and selected columns
58
+ */
59
+ function createCacheKey(
60
+ document: DocumentNode,
61
+ selectedColumns: Array<{ name: string; isCustomField: boolean; dependencies?: string[] }>,
62
+ ): string {
63
+ const docId = getDocumentId(document);
64
+ const columnsKey = sortJoin(
65
+ selectedColumns.map(col => {
66
+ const deps = col.dependencies ? sortJoin(col.dependencies, '+') : '';
67
+ const depsPart = deps ? `:deps(${deps})` : '';
68
+ return `${col.name}:${String(col.isCustomField)}${depsPart}`;
69
+ }),
70
+ ',',
71
+ );
72
+ return `${docId}|${columnsKey}`;
73
+ }
74
+
75
+ function sortJoin<T>(arr: T[], separator: string): string {
76
+ return arr.slice(0).sort().join(separator);
77
+ }
78
+
79
+ /**
80
+ * @description
81
+ * This function takes a list query document such as:
82
+ * ```gql
83
+ * query ProductList($options: ProductListOptions) {
84
+ * products(options: $options) {
85
+ * items {
86
+ * id
87
+ * createdAt
88
+ * updatedAt
89
+ * featuredAsset {
90
+ * id
91
+ * preview
92
+ * }
93
+ * name
94
+ * slug
95
+ * enabled
96
+ * }
97
+ * totalItems
98
+ * }
99
+ * }
100
+ * ```
101
+ * and an array of selected columns, and returns a new document which only selects the
102
+ * specified columns. So if `selectedColumns` equals `[{ name: 'id', isCustomField: false }]`,
103
+ * then the resulting document's `items` fields would be `{ id }`.
104
+ *
105
+ * Columns can also declare dependencies on other fields that are required for rendering
106
+ * but not necessarily visible. For example:
107
+ * ```js
108
+ * selectedColumns = [{
109
+ * name: 'name',
110
+ * isCustomField: false,
111
+ * dependencies: ['children', 'breadcrumbs'] // Always include these fields
112
+ * }]
113
+ * ```
114
+ * This ensures that cell renderers can safely access dependent fields even when they're
115
+ * not part of the visible column set.
116
+ *
117
+ * @param listQuery The GraphQL document to filter
118
+ * @param selectedColumns Array of column definitions with optional dependencies
119
+ */
120
+ export function includeOnlySelectedListFields<T extends DocumentNode>(
121
+ listQuery: T,
122
+ selectedColumns: Array<{
123
+ name: string;
124
+ isCustomField: boolean;
125
+ dependencies?: string[];
126
+ }>,
127
+ ): T {
128
+ // If no columns selected, return the original document
129
+ if (selectedColumns.length === 0) {
130
+ return listQuery;
131
+ }
132
+
133
+ // Check cache first
134
+ const cacheKey = createCacheKey(listQuery, selectedColumns);
135
+ if (filterCache.has(cacheKey)) {
136
+ return filterCache.get(cacheKey) as T;
137
+ }
138
+
139
+ // Get the query name to identify the main list query field
140
+ const queryName = getQueryName(listQuery);
141
+
142
+ // Collect all required fields including dependencies
143
+ const allRequiredFields = new Set<string>();
144
+ const customFieldNames = new Set<string>();
145
+
146
+ selectedColumns.forEach(col => {
147
+ allRequiredFields.add(col.name);
148
+ if (col.isCustomField) {
149
+ customFieldNames.add(col.name);
150
+ }
151
+ // Add dependencies
152
+ col.dependencies?.forEach(dep => {
153
+ allRequiredFields.add(dep);
154
+ // Note: Dependencies are assumed to be regular fields unless they start with custom field patterns
155
+ });
156
+ });
157
+
158
+ const selectedFieldNames = allRequiredFields;
159
+
160
+ // Collect all fragments from the document
161
+ const fragments = collectFragments(listQuery);
162
+
163
+ // First pass: identify which fragments are directly used by the items field
164
+ const itemsFragments = getItemsFragments(listQuery, queryName);
165
+
166
+ // Visit and transform the document
167
+ const modifiedDocument = visit(listQuery, {
168
+ [Kind.FIELD]: {
169
+ enter(node: FieldNode, key, parent, path, ancestors): FieldNode | undefined {
170
+ // Check if we're at the query root field (e.g., "products")
171
+ const isQueryRoot =
172
+ ancestors.some(
173
+ ancestor =>
174
+ ancestor &&
175
+ typeof ancestor === 'object' &&
176
+ 'kind' in ancestor &&
177
+ ancestor.kind === Kind.OPERATION_DEFINITION &&
178
+ ancestor.operation === 'query',
179
+ ) && node.name.value === queryName;
180
+
181
+ if (!isQueryRoot) {
182
+ return undefined;
183
+ }
184
+
185
+ // Look for the "items" field within the query root
186
+ if (node.selectionSet) {
187
+ const modifiedSelections = node.selectionSet.selections.map(selection => {
188
+ if (isFieldWithName(selection, 'items')) {
189
+ // Filter the items field to only include selected columns
190
+ return filterItemsField(
191
+ selection,
192
+ selectedFieldNames,
193
+ customFieldNames,
194
+ fragments,
195
+ );
196
+ }
197
+ return selection;
198
+ });
199
+
200
+ return {
201
+ ...node,
202
+ selectionSet: {
203
+ ...node.selectionSet,
204
+ selections: modifiedSelections,
205
+ },
206
+ };
207
+ }
208
+
209
+ return undefined;
210
+ },
211
+ },
212
+ [Kind.FRAGMENT_DEFINITION]: {
213
+ enter(node: FragmentDefinitionNode): FragmentDefinitionNode {
214
+ // Only filter fragments that are directly used by the items field
215
+ if (itemsFragments.has(node.name.value)) {
216
+ return filterFragment(node, selectedFieldNames, customFieldNames, fragments);
217
+ }
218
+ // Leave other fragments untouched
219
+ return node;
220
+ },
221
+ },
222
+ });
223
+
224
+ // Remove unused fragments to prevent GraphQL validation errors
225
+ const withoutUnusedFragments = removeUnusedFragments(modifiedDocument);
226
+
227
+ // Remove unused variables to prevent GraphQL validation errors
228
+ const result = removeUnusedVariables(withoutUnusedFragments);
229
+
230
+ // Cache the result with LRU eviction
231
+ if (filterCache.size >= MAX_CACHE_SIZE) {
232
+ const firstKey = filterCache.keys().next().value;
233
+ if (firstKey) {
234
+ filterCache.delete(firstKey);
235
+ }
236
+ }
237
+ filterCache.set(cacheKey, result);
238
+ return result;
239
+ }
240
+
241
+ /**
242
+ * Collect all fragments from the document
243
+ */
244
+ function collectFragments(document: DocumentNode): Record<string, FragmentDefinitionNode> {
245
+ const fragments: Record<string, FragmentDefinitionNode> = {};
246
+ for (const definition of document.definitions) {
247
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) {
248
+ fragments[definition.name.value] = definition;
249
+ }
250
+ }
251
+ return fragments;
252
+ }
253
+
254
+ /**
255
+ * Check if a selection is a field with the given name
256
+ */
257
+ function isFieldWithName(selection: SelectionNode, fieldName: string): selection is FieldNode {
258
+ return selection.kind === Kind.FIELD && selection.name.value === fieldName;
259
+ }
260
+
261
+ /**
262
+ * Check if a selection is a field with the given name and has a selection set
263
+ */
264
+ function isFieldWithNameAndSelections(selection: SelectionNode, fieldName: string): selection is FieldNode {
265
+ return isFieldWithName(selection, fieldName) && !!selection.selectionSet;
266
+ }
267
+
268
+ /**
269
+ * Collect fragment spreads from a selection set
270
+ */
271
+ function collectFragmentSpreads(selections: readonly SelectionNode[]): string[] {
272
+ const fragmentNames: string[] = [];
273
+ for (const selection of selections) {
274
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
275
+ fragmentNames.push(selection.name.value);
276
+ }
277
+ }
278
+ return fragmentNames;
279
+ }
280
+
281
+ /**
282
+ * Check if a selection is a field node
283
+ */
284
+ function isField(selection: SelectionNode): selection is FieldNode {
285
+ return selection.kind === Kind.FIELD;
286
+ }
287
+
288
+ /**
289
+ * Find the items field within a query field's selections
290
+ */
291
+ function findItemsFieldFragments(querySelections: readonly SelectionNode[]): string[] {
292
+ for (const selection of querySelections) {
293
+ if (isFieldWithNameAndSelections(selection, 'items') && selection.selectionSet) {
294
+ return collectFragmentSpreads(selection.selectionSet.selections);
295
+ }
296
+ }
297
+ return [];
298
+ }
299
+
300
+ /**
301
+ * Find the query field with the given name and process its items field
302
+ */
303
+ function findQueryFieldFragments(selections: readonly SelectionNode[], queryName: string): string[] {
304
+ for (const selection of selections) {
305
+ if (isFieldWithNameAndSelections(selection, queryName) && selection.selectionSet) {
306
+ return findItemsFieldFragments(selection.selectionSet.selections);
307
+ }
308
+ }
309
+ return [];
310
+ }
311
+
312
+ /**
313
+ * Get fragments that are directly used by the items field (not nested fragments)
314
+ */
315
+ function getItemsFragments(document: DocumentNode, queryName: string): Set<string> {
316
+ const itemsFragments = new Set<string>();
317
+
318
+ for (const definition of document.definitions) {
319
+ if (definition.kind === Kind.OPERATION_DEFINITION && definition.operation === 'query') {
320
+ const fragmentNames = findQueryFieldFragments(definition.selectionSet.selections, queryName);
321
+ fragmentNames.forEach(name => itemsFragments.add(name));
322
+ }
323
+ }
324
+
325
+ return itemsFragments;
326
+ }
327
+
328
+ /**
329
+ * Context for filtering field selections
330
+ */
331
+ interface FieldSelectionContext {
332
+ itemsField: FieldNode;
333
+ availableFields: Map<string, SelectionNode>;
334
+ fragmentSpreads: Map<string, FragmentSpreadNode>;
335
+ fragments: Record<string, FragmentDefinitionNode>;
336
+ neededFragments: Set<string>;
337
+ filteredSelections: SelectionNode[];
338
+ }
339
+
340
+ /**
341
+ * Check if a field is selected directly in the items field (not from a fragment)
342
+ */
343
+ function isDirectField(fieldName: string, itemsField: FieldNode): boolean {
344
+ if (!itemsField.selectionSet) return false;
345
+ return itemsField.selectionSet.selections.some(sel => isFieldWithName(sel, fieldName));
346
+ }
347
+
348
+ /**
349
+ * Add a regular field to filtered selections
350
+ */
351
+ function addRegularField(fieldName: string, context: FieldSelectionContext): void {
352
+ if (!context.availableFields.has(fieldName)) return;
353
+
354
+ if (isDirectField(fieldName, context.itemsField)) {
355
+ const fieldSelection = context.availableFields.get(fieldName);
356
+ if (fieldSelection) {
357
+ context.filteredSelections.push(fieldSelection);
358
+ }
359
+ } else {
360
+ // Field comes from a fragment - mark fragments as needed
361
+ for (const [fragName] of context.fragmentSpreads) {
362
+ const fragment = context.fragments[fragName];
363
+ if (fragment && fragmentContainsField(fragment, fieldName, context.fragments)) {
364
+ context.neededFragments.add(fragName);
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Add custom fields to filtered selections
372
+ */
373
+ function addCustomFields(customFieldNames: Set<string>, context: FieldSelectionContext): void {
374
+ if (customFieldNames.size === 0 || !context.availableFields.has('customFields')) return;
375
+
376
+ if (isDirectField('customFields', context.itemsField)) {
377
+ const customFieldsSelection = context.availableFields.get('customFields');
378
+ if (customFieldsSelection && customFieldsSelection.kind === Kind.FIELD) {
379
+ const filteredCustomFields = filterCustomFields(customFieldsSelection, customFieldNames);
380
+ if (filteredCustomFields) {
381
+ context.filteredSelections.push(filteredCustomFields);
382
+ }
383
+ }
384
+ } else {
385
+ // customFields comes from a fragment
386
+ for (const [fragName] of context.fragmentSpreads) {
387
+ const fragment = context.fragments[fragName];
388
+ if (fragment && fragmentContainsField(fragment, 'customFields', context.fragments)) {
389
+ context.neededFragments.add(fragName);
390
+ }
391
+ }
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Check if context has only __typename as a field (which doesn't count as valid content)
397
+ */
398
+ function hasOnlyTypenameField(context: FieldSelectionContext): boolean {
399
+ return (
400
+ context.filteredSelections.length === 1 &&
401
+ isFieldWithName(context.filteredSelections[0], '__typename')
402
+ );
403
+ }
404
+
405
+ /**
406
+ * Check if context has valid fields that make the query meaningful
407
+ */
408
+ function hasValidFields(context: FieldSelectionContext): boolean {
409
+ return context.filteredSelections.length > 0 && !hasOnlyTypenameField(context);
410
+ }
411
+
412
+ /**
413
+ * Add id field directly from available fields
414
+ */
415
+ function addDirectIdField(context: FieldSelectionContext): void {
416
+ const idField = context.availableFields.get('id');
417
+ if (idField) {
418
+ context.filteredSelections.push(idField);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Add id field from a fragment that contains it
424
+ */
425
+ function addIdFieldFromFragment(context: FieldSelectionContext): void {
426
+ for (const [fragName] of context.fragmentSpreads) {
427
+ const fragment = context.fragments[fragName];
428
+ if (fragment && fragmentContainsField(fragment, 'id', context.fragments)) {
429
+ const fragmentSpread = context.fragmentSpreads.get(fragName);
430
+ if (fragmentSpread) {
431
+ context.filteredSelections.push(fragmentSpread);
432
+ }
433
+ break;
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Create a minimal id field when none exists
440
+ */
441
+ function createMinimalIdField(context: FieldSelectionContext): void {
442
+ context.filteredSelections.push({
443
+ kind: Kind.FIELD,
444
+ name: { kind: Kind.NAME, value: 'id' },
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Ensure at least id field is included to maintain valid query
450
+ */
451
+ function ensureIdField(context: FieldSelectionContext): void {
452
+ if (hasValidFields(context)) return;
453
+
454
+ if (context.availableFields.has('id')) {
455
+ if (isDirectField('id', context.itemsField)) {
456
+ addDirectIdField(context);
457
+ } else {
458
+ addIdFieldFromFragment(context);
459
+ }
460
+ } else {
461
+ createMinimalIdField(context);
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Initialize field selection context
467
+ */
468
+ function initializeFieldSelectionContext(
469
+ itemsField: FieldNode,
470
+ fragments: Record<string, FragmentDefinitionNode>,
471
+ ): FieldSelectionContext {
472
+ return {
473
+ itemsField,
474
+ availableFields: new Map(),
475
+ fragmentSpreads: new Map(),
476
+ fragments,
477
+ neededFragments: new Set(),
478
+ filteredSelections: [],
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Collect available fields and fragment spreads from selections
484
+ */
485
+ function collectAvailableSelections(
486
+ context: FieldSelectionContext,
487
+ selections: readonly SelectionNode[],
488
+ fragments: Record<string, FragmentDefinitionNode>,
489
+ ): void {
490
+ for (const selection of selections) {
491
+ if (isField(selection)) {
492
+ context.availableFields.set(selection.name.value, selection);
493
+ } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
494
+ context.fragmentSpreads.set(selection.name.value, selection);
495
+ const fragment = fragments[selection.name.value];
496
+ if (fragment) {
497
+ collectFieldsFromFragment(fragment, fragments, context.availableFields);
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Add __typename field if it exists in original selections
505
+ */
506
+ function addTypenameField(context: FieldSelectionContext): void {
507
+ if (context.availableFields.has('__typename')) {
508
+ const typenameField = context.availableFields.get('__typename');
509
+ if (typenameField) {
510
+ context.filteredSelections.push(typenameField);
511
+ }
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Add all selected regular fields
517
+ */
518
+ function addSelectedFields(context: FieldSelectionContext, selectedFieldNames: Set<string>): void {
519
+ for (const fieldName of selectedFieldNames) {
520
+ if (fieldName === '__typename') continue;
521
+ addRegularField(fieldName, context);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Add needed fragment spreads to filtered selections
527
+ */
528
+ function addNeededFragmentSpreads(context: FieldSelectionContext): void {
529
+ for (const fragName of context.neededFragments) {
530
+ const fragmentSpread = context.fragmentSpreads.get(fragName);
531
+ if (fragmentSpread) {
532
+ context.filteredSelections.push(fragmentSpread);
533
+ }
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Add inline fragments from original selections
539
+ */
540
+ function addInlineFragments(context: FieldSelectionContext, selections: readonly SelectionNode[]): void {
541
+ for (const selection of selections) {
542
+ if (selection.kind === Kind.INLINE_FRAGMENT) {
543
+ context.filteredSelections.push(selection);
544
+ }
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Filter the items field to only include selected columns
550
+ */
551
+ function filterItemsField(
552
+ itemsField: FieldNode,
553
+ selectedFieldNames: Set<string>,
554
+ customFieldNames: Set<string>,
555
+ fragments: Record<string, FragmentDefinitionNode>,
556
+ ): FieldNode {
557
+ if (!itemsField.selectionSet) {
558
+ return itemsField;
559
+ }
560
+
561
+ const context = initializeFieldSelectionContext(itemsField, fragments);
562
+
563
+ collectAvailableSelections(context, itemsField.selectionSet.selections, fragments);
564
+ addTypenameField(context);
565
+ addSelectedFields(context, selectedFieldNames);
566
+ addCustomFields(customFieldNames, context);
567
+ addNeededFragmentSpreads(context);
568
+ addInlineFragments(context, itemsField.selectionSet.selections);
569
+ ensureIdField(context);
570
+
571
+ return {
572
+ ...itemsField,
573
+ selectionSet: {
574
+ ...itemsField.selectionSet,
575
+ selections: context.filteredSelections,
576
+ },
577
+ };
578
+ }
579
+
580
+ /**
581
+ * Collect all fields from a fragment recursively
582
+ */
583
+ function collectFieldsFromFragment(
584
+ fragment: FragmentDefinitionNode,
585
+ fragments: Record<string, FragmentDefinitionNode>,
586
+ availableFields: Map<string, SelectionNode>,
587
+ ): void {
588
+ for (const selection of fragment.selectionSet.selections) {
589
+ if (isField(selection)) {
590
+ availableFields.set(selection.name.value, selection);
591
+ } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
592
+ const nestedFragment = fragments[selection.name.value];
593
+ if (nestedFragment) {
594
+ collectFieldsFromFragment(nestedFragment, fragments, availableFields);
595
+ }
596
+ }
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Check if a fragment contains a specific field
602
+ */
603
+ function fragmentContainsField(
604
+ fragment: FragmentDefinitionNode,
605
+ fieldName: string,
606
+ fragments: Record<string, FragmentDefinitionNode>,
607
+ ): boolean {
608
+ for (const selection of fragment.selectionSet.selections) {
609
+ if (isFieldWithName(selection, fieldName)) {
610
+ return true;
611
+ } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
612
+ const nestedFragment = fragments[selection.name.value];
613
+ if (nestedFragment && fragmentContainsField(nestedFragment, fieldName, fragments)) {
614
+ return true;
615
+ }
616
+ }
617
+ }
618
+ return false;
619
+ }
620
+
621
+ /**
622
+ * Process a field selection for fragment filtering
623
+ */
624
+ function processFragmentFieldSelection(
625
+ selection: FieldNode,
626
+ selectedFieldNames: Set<string>,
627
+ customFieldNames: Set<string>,
628
+ filteredSelections: SelectionNode[],
629
+ ): void {
630
+ const fieldName = selection.name.value;
631
+
632
+ if (fieldName === '__typename') {
633
+ filteredSelections.push(selection);
634
+ } else if (selectedFieldNames.has(fieldName)) {
635
+ filteredSelections.push(selection);
636
+ } else if (fieldName === 'customFields' && customFieldNames.size > 0) {
637
+ const filteredCustomFields = filterCustomFields(selection, customFieldNames);
638
+ if (filteredCustomFields) {
639
+ filteredSelections.push(filteredCustomFields);
640
+ }
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Check if a fragment spread contains any selected fields
646
+ */
647
+ function fragmentSpreadContainsSelectedFields(
648
+ spreadFragment: FragmentDefinitionNode,
649
+ selectedFieldNames: Set<string>,
650
+ fragments: Record<string, FragmentDefinitionNode>,
651
+ ): boolean {
652
+ for (const fieldName of selectedFieldNames) {
653
+ if (fragmentContainsField(spreadFragment, fieldName, fragments)) {
654
+ return true;
655
+ }
656
+ }
657
+ return false;
658
+ }
659
+
660
+ /**
661
+ * Process a fragment spread selection for fragment filtering
662
+ */
663
+ function processFragmentSpreadSelection(
664
+ selection: FragmentSpreadNode,
665
+ selectedFieldNames: Set<string>,
666
+ fragments: Record<string, FragmentDefinitionNode>,
667
+ filteredSelections: SelectionNode[],
668
+ ): void {
669
+ const spreadFragment = fragments[selection.name.value];
670
+ if (
671
+ spreadFragment &&
672
+ fragmentSpreadContainsSelectedFields(spreadFragment, selectedFieldNames, fragments)
673
+ ) {
674
+ filteredSelections.push(selection);
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Process all selections in a fragment
680
+ */
681
+ function processFragmentSelections(
682
+ selections: readonly SelectionNode[],
683
+ selectedFieldNames: Set<string>,
684
+ customFieldNames: Set<string>,
685
+ fragments: Record<string, FragmentDefinitionNode>,
686
+ ): SelectionNode[] {
687
+ const filteredSelections: SelectionNode[] = [];
688
+
689
+ for (const selection of selections) {
690
+ if (isField(selection)) {
691
+ processFragmentFieldSelection(
692
+ selection,
693
+ selectedFieldNames,
694
+ customFieldNames,
695
+ filteredSelections,
696
+ );
697
+ } else if (selection.kind === Kind.FRAGMENT_SPREAD) {
698
+ processFragmentSpreadSelection(selection, selectedFieldNames, fragments, filteredSelections);
699
+ } else if (selection.kind === Kind.INLINE_FRAGMENT) {
700
+ // Keep inline fragments for now - more complex filtering would need type info
701
+ filteredSelections.push(selection);
702
+ }
703
+ }
704
+
705
+ return filteredSelections;
706
+ }
707
+
708
+ /**
709
+ * Ensure fragment has at least one field by adding id if available
710
+ */
711
+ function ensureFragmentHasFields(
712
+ filteredSelections: SelectionNode[],
713
+ originalSelections: readonly SelectionNode[],
714
+ ): void {
715
+ if (filteredSelections.length === 0) {
716
+ // Add id if it exists in the original fragment
717
+ for (const selection of originalSelections) {
718
+ if (isFieldWithName(selection, 'id')) {
719
+ filteredSelections.push(selection);
720
+ break;
721
+ }
722
+ }
723
+ }
724
+ }
725
+
726
+ /**
727
+ * Filter a fragment to only include selected fields
728
+ */
729
+ function filterFragment(
730
+ fragment: FragmentDefinitionNode,
731
+ selectedFieldNames: Set<string>,
732
+ customFieldNames: Set<string>,
733
+ fragments: Record<string, FragmentDefinitionNode>,
734
+ ): FragmentDefinitionNode {
735
+ const filteredSelections = processFragmentSelections(
736
+ fragment.selectionSet.selections,
737
+ selectedFieldNames,
738
+ customFieldNames,
739
+ fragments,
740
+ );
741
+
742
+ ensureFragmentHasFields(filteredSelections, fragment.selectionSet.selections);
743
+
744
+ return {
745
+ ...fragment,
746
+ selectionSet: {
747
+ ...fragment.selectionSet,
748
+ selections: filteredSelections,
749
+ },
750
+ };
751
+ }
752
+
753
+ /**
754
+ * Filter the customFields selection to only include selected custom fields
755
+ */
756
+ function filterCustomFields(customFieldsNode: FieldNode, customFieldNames: Set<string>): FieldNode | null {
757
+ if (!customFieldsNode.selectionSet) {
758
+ return customFieldsNode;
759
+ }
760
+
761
+ const filteredSelections = customFieldsNode.selectionSet.selections.filter(selection => {
762
+ if (isField(selection)) {
763
+ return customFieldNames.has(selection.name.value);
764
+ }
765
+ // Keep fragments as they might contain selected custom fields
766
+ return true;
767
+ });
768
+
769
+ if (filteredSelections.length === 0) {
770
+ return null;
771
+ }
772
+
773
+ return {
774
+ ...customFieldsNode,
775
+ selectionSet: {
776
+ ...customFieldsNode.selectionSet,
777
+ selections: filteredSelections,
778
+ },
779
+ };
780
+ }
781
+
782
+ /**
783
+ * Remove unused fragments from the document to prevent GraphQL validation errors
784
+ */
785
+ function removeUnusedFragments<T extends DocumentNode>(document: T): T {
786
+ // First, collect all fragment names that are actually used in the document
787
+ const usedFragments = new Set<string>();
788
+
789
+ // Helper function to recursively find fragment spreads
790
+ const findFragmentSpreads = (selections: readonly SelectionNode[]) => {
791
+ for (const selection of selections) {
792
+ if (selection.kind === Kind.FRAGMENT_SPREAD) {
793
+ usedFragments.add(selection.name.value);
794
+ } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.selectionSet) {
795
+ findFragmentSpreads(selection.selectionSet.selections);
796
+ } else if (selection.kind === Kind.FIELD && selection.selectionSet) {
797
+ findFragmentSpreads(selection.selectionSet.selections);
798
+ }
799
+ }
800
+ };
801
+
802
+ // Look through all operations to find used fragments
803
+ for (const definition of document.definitions) {
804
+ if (definition.kind === Kind.OPERATION_DEFINITION) {
805
+ findFragmentSpreads(definition.selectionSet.selections);
806
+ }
807
+ }
808
+
809
+ // Now we need to handle transitive dependencies - fragments that use other fragments
810
+ let foundNewFragments = true;
811
+ while (foundNewFragments) {
812
+ foundNewFragments = false;
813
+ for (const definition of document.definitions) {
814
+ if (definition.kind === Kind.FRAGMENT_DEFINITION && usedFragments.has(definition.name.value)) {
815
+ const previousSize = usedFragments.size;
816
+ findFragmentSpreads(definition.selectionSet.selections);
817
+ if (usedFragments.size > previousSize) {
818
+ foundNewFragments = true;
819
+ }
820
+ }
821
+ }
822
+ }
823
+
824
+ // Filter out unused fragment definitions
825
+ const filteredDefinitions = document.definitions.filter(definition => {
826
+ if (definition.kind === Kind.FRAGMENT_DEFINITION) {
827
+ return usedFragments.has(definition.name.value);
828
+ }
829
+ return true;
830
+ });
831
+
832
+ return {
833
+ ...document,
834
+ definitions: filteredDefinitions,
835
+ } as T;
836
+ }
837
+
838
+ /**
839
+ * Remove unused variables from the document to prevent GraphQL validation errors
840
+ */
841
+ function removeUnusedVariables<T extends DocumentNode>(document: T): T {
842
+ const collector = new VariableUsageCollector();
843
+ const usedVariables = collector.collectFromDocument(document);
844
+
845
+ // Filter out unused variable definitions from operations
846
+ const modifiedDefinitions = document.definitions.map(definition => {
847
+ if (definition.kind === Kind.OPERATION_DEFINITION && definition.variableDefinitions) {
848
+ const filteredVariableDefinitions = definition.variableDefinitions.filter(variableDef =>
849
+ usedVariables.has(variableDef.variable.name.value),
850
+ );
851
+
852
+ return {
853
+ ...definition,
854
+ variableDefinitions: filteredVariableDefinitions,
855
+ };
856
+ }
857
+ return definition;
858
+ });
859
+
860
+ return {
861
+ ...document,
862
+ definitions: modifiedDefinitions,
863
+ } as T;
864
+ }
865
+
866
+ /**
867
+ * Variable usage collector that traverses GraphQL structures
868
+ */
869
+ class VariableUsageCollector {
870
+ private readonly usedVariables = new Set<string>();
871
+
872
+ /**
873
+ * Collect variables from a GraphQL value (recursive)
874
+ */
875
+ private collectFromValue(value: any): void {
876
+ switch (value.kind) {
877
+ case Kind.VARIABLE:
878
+ this.usedVariables.add((value as VariableNode).name.value);
879
+ break;
880
+ case Kind.LIST:
881
+ value.values.forEach((item: any) => this.collectFromValue(item));
882
+ break;
883
+ case Kind.OBJECT:
884
+ value.fields.forEach((field: any) => this.collectFromValue(field.value));
885
+ break;
886
+ // For other value types (STRING, INT, FLOAT, BOOLEAN, NULL, ENUM), no variables to collect
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Collect variables from field arguments
892
+ */
893
+ private collectFromArguments(args: readonly ArgumentNode[]): void {
894
+ args.forEach(arg => this.collectFromValue(arg.value));
895
+ }
896
+
897
+ /**
898
+ * Collect variables from selection set (recursive)
899
+ */
900
+ private collectFromSelections(selections: readonly SelectionNode[]): void {
901
+ selections.forEach(selection => {
902
+ switch (selection.kind) {
903
+ case Kind.FIELD:
904
+ if (selection.arguments) {
905
+ this.collectFromArguments(selection.arguments);
906
+ }
907
+ if (selection.selectionSet) {
908
+ this.collectFromSelections(selection.selectionSet.selections);
909
+ }
910
+ break;
911
+ case Kind.INLINE_FRAGMENT:
912
+ if (selection.selectionSet) {
913
+ this.collectFromSelections(selection.selectionSet.selections);
914
+ }
915
+ break;
916
+ case Kind.FRAGMENT_SPREAD:
917
+ // Fragment spreads are handled when processing fragment definitions
918
+ break;
919
+ }
920
+ });
921
+ }
922
+
923
+ /**
924
+ * Collect all used variables from a document
925
+ */
926
+ collectFromDocument(document: DocumentNode): Set<string> {
927
+ this.usedVariables.clear();
928
+
929
+ document.definitions.forEach(definition => {
930
+ if (
931
+ definition.kind === Kind.OPERATION_DEFINITION ||
932
+ definition.kind === Kind.FRAGMENT_DEFINITION
933
+ ) {
934
+ this.collectFromSelections(definition.selectionSet.selections);
935
+ }
936
+ });
937
+
938
+ return new Set(this.usedVariables);
939
+ }
940
+ }