@vendure/dashboard 3.5.0-minor-202510031341 → 3.5.0-minor-202510161257

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 (220) hide show
  1. package/dist/plugin/dashboard.plugin.js +1 -1
  2. package/dist/plugin/default-page.html +1 -1
  3. package/dist/vite/utils/ast-utils.spec.js +3 -3
  4. package/dist/vite/utils/tsconfig-utils.js +2 -1
  5. package/dist/vite/vite-plugin-hmr.d.ts +8 -0
  6. package/dist/vite/vite-plugin-hmr.js +34 -0
  7. package/dist/vite/vite-plugin-theme.js +6 -6
  8. package/dist/vite/vite-plugin-transform-index.js +6 -1
  9. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
  10. package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
  11. package/package.json +18 -5
  12. package/src/app/app-providers.tsx +4 -1
  13. package/src/app/common/map-faceted-filter-fields.ts +21 -0
  14. package/src/app/main.tsx +3 -1
  15. package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
  16. package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
  17. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
  18. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
  19. package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
  20. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
  21. package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
  22. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
  23. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
  24. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
  25. package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
  26. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
  27. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
  28. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +4 -8
  29. package/src/app/routes/_authenticated/_global-settings/utils/global-languages.ts +268 -0
  30. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
  31. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +15 -15
  32. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +5 -5
  33. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
  34. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
  35. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
  36. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
  37. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
  38. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
  39. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
  40. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
  41. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
  42. package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
  43. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
  44. package/src/app/routes/_authenticated/_product-variants/components/add-currency-dropdown.tsx +49 -0
  45. package/src/app/routes/_authenticated/_product-variants/components/add-stock-location-dropdown.tsx +56 -0
  46. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +12 -0
  47. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +178 -50
  48. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
  49. package/src/app/routes/_authenticated/_products/components/product-variants-table.tsx +0 -11
  50. package/src/app/routes/_authenticated/_products/products.tsx +6 -2
  51. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
  52. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
  53. package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
  54. package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
  55. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +3 -10
  56. package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
  57. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
  58. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
  59. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
  60. package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
  61. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
  62. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
  63. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
  64. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
  65. package/src/app/routes/login.tsx +2 -2
  66. package/src/i18n/locales/ar.po +420 -289
  67. package/src/i18n/locales/cs.po +420 -289
  68. package/src/i18n/locales/de.po +420 -289
  69. package/src/i18n/locales/en.po +420 -289
  70. package/src/i18n/locales/es.po +420 -289
  71. package/src/i18n/locales/fa.po +420 -289
  72. package/src/i18n/locales/fr.po +468 -337
  73. package/src/i18n/locales/he.po +420 -289
  74. package/src/i18n/locales/hr.po +420 -289
  75. package/src/i18n/locales/it.po +420 -289
  76. package/src/i18n/locales/ja.po +420 -289
  77. package/src/i18n/locales/nb.po +420 -289
  78. package/src/i18n/locales/ne.po +420 -289
  79. package/src/i18n/locales/pl.po +420 -289
  80. package/src/i18n/locales/pt_BR.po +420 -289
  81. package/src/i18n/locales/pt_PT.po +420 -289
  82. package/src/i18n/locales/ru.po +420 -289
  83. package/src/i18n/locales/sv.po +420 -289
  84. package/src/i18n/locales/tr.po +420 -289
  85. package/src/i18n/locales/uk.po +420 -289
  86. package/src/i18n/locales/zh_Hans.po +420 -289
  87. package/src/i18n/locales/zh_Hant.po +420 -289
  88. package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
  89. package/src/lib/components/data-input/affixed-input.tsx +5 -2
  90. package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
  91. package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
  92. package/src/lib/components/data-input/customer-group-input.tsx +0 -1
  93. package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
  94. package/src/lib/components/data-input/datetime-input.tsx +27 -13
  95. package/src/lib/components/data-input/default-relation-input.tsx +18 -12
  96. package/src/lib/components/data-input/money-input.stories.tsx +88 -0
  97. package/src/lib/components/data-input/money-input.tsx +7 -11
  98. package/src/lib/components/data-input/number-input.stories.tsx +103 -0
  99. package/src/lib/components/data-input/number-input.tsx +16 -5
  100. package/src/lib/components/data-input/password-input.stories.tsx +65 -0
  101. package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
  102. package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
  103. package/src/lib/components/data-input/slug-input.tsx +9 -10
  104. package/src/lib/components/data-input/text-input.stories.tsx +52 -0
  105. package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
  106. package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
  107. package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
  108. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
  109. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  110. package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
  111. package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
  112. package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
  113. package/src/lib/components/data-table/data-table-filter-badge.tsx +28 -14
  114. package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
  115. package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
  116. package/src/lib/components/data-table/data-table.stories.tsx +249 -0
  117. package/src/lib/components/data-table/data-table.tsx +39 -11
  118. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
  119. package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
  120. package/src/lib/components/layout/generated-breadcrumbs.tsx +4 -12
  121. package/src/lib/components/layout/nav-user.tsx +19 -13
  122. package/src/lib/components/login/login-form.tsx +39 -123
  123. package/src/lib/components/shared/alerts.tsx +29 -17
  124. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
  125. package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
  126. package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
  127. package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
  128. package/src/lib/components/shared/configurable-operation-input.tsx +1 -1
  129. package/src/lib/components/shared/customer-group-selector.tsx +5 -2
  130. package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
  131. package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
  132. package/src/lib/components/shared/facet-value-selector.tsx +130 -34
  133. package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
  134. package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
  135. package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
  136. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
  137. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
  138. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
  139. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
  140. package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
  141. package/src/lib/components/shared/vendure-image.tsx +6 -7
  142. package/src/lib/components/ui/accordion.stories.tsx +33 -0
  143. package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
  144. package/src/lib/components/ui/alert.stories.tsx +35 -0
  145. package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
  146. package/src/lib/components/ui/badge.stories.tsx +28 -0
  147. package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
  148. package/src/lib/components/ui/button.stories.tsx +38 -0
  149. package/src/lib/components/ui/calendar.stories.tsx +22 -0
  150. package/src/lib/components/ui/card.stories.tsx +28 -0
  151. package/src/lib/components/ui/carousel.stories.tsx +34 -0
  152. package/src/lib/components/ui/checkbox.stories.tsx +31 -0
  153. package/src/lib/components/ui/collapsible.stories.tsx +39 -0
  154. package/src/lib/components/ui/command.stories.tsx +44 -0
  155. package/src/lib/components/ui/context-menu.stories.tsx +38 -0
  156. package/src/lib/components/ui/dialog.stories.tsx +52 -0
  157. package/src/lib/components/ui/drawer.stories.tsx +50 -0
  158. package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
  159. package/src/lib/components/ui/hover-card.stories.tsx +38 -0
  160. package/src/lib/components/ui/input-group.tsx +148 -0
  161. package/src/lib/components/ui/input-otp.stories.tsx +30 -0
  162. package/src/lib/components/ui/input.stories.tsx +38 -0
  163. package/src/lib/components/ui/label.stories.tsx +24 -0
  164. package/src/lib/components/ui/menubar.stories.tsx +53 -0
  165. package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
  166. package/src/lib/components/ui/pagination.stories.tsx +51 -0
  167. package/src/lib/components/ui/password-input.stories.tsx +32 -0
  168. package/src/lib/components/ui/password-input.tsx +33 -0
  169. package/src/lib/components/ui/popover.stories.tsx +33 -0
  170. package/src/lib/components/ui/progress.stories.tsx +27 -0
  171. package/src/lib/components/ui/radio-group.stories.tsx +34 -0
  172. package/src/lib/components/ui/resizable.stories.tsx +32 -0
  173. package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
  174. package/src/lib/components/ui/select.stories.tsx +36 -0
  175. package/src/lib/components/ui/separator.stories.tsx +35 -0
  176. package/src/lib/components/ui/sheet.stories.tsx +50 -0
  177. package/src/lib/components/ui/sidebar-context.ts +16 -0
  178. package/src/lib/components/ui/sidebar.tsx +2 -13
  179. package/src/lib/components/ui/skeleton.stories.tsx +26 -0
  180. package/src/lib/components/ui/slider.stories.tsx +37 -0
  181. package/src/lib/components/ui/switch.stories.tsx +31 -0
  182. package/src/lib/components/ui/table.stories.tsx +52 -0
  183. package/src/lib/components/ui/tabs.stories.tsx +29 -0
  184. package/src/lib/components/ui/textarea.stories.tsx +32 -0
  185. package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
  186. package/src/lib/components/ui/toggle.stories.tsx +39 -0
  187. package/src/lib/components/ui/tooltip.stories.tsx +30 -0
  188. package/src/lib/components/ui/tooltip.tsx +2 -2
  189. package/src/lib/framework/alert/alert-extensions.tsx +0 -11
  190. package/src/lib/framework/alert/alert-item.tsx +14 -19
  191. package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
  192. package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
  193. package/src/lib/framework/component-registry/component-registry.tsx +3 -14
  194. package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
  195. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +0 -2
  196. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
  197. package/src/lib/framework/defaults.ts +9 -13
  198. package/src/lib/framework/extension-api/input-component-extensions.tsx +6 -1
  199. package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
  200. package/src/lib/framework/extension-api/types/alerts.ts +12 -6
  201. package/src/lib/framework/extension-api/types/data-table.ts +5 -2
  202. package/src/lib/framework/extension-api/types/layout.ts +41 -1
  203. package/src/lib/framework/extension-api/types/login.ts +0 -21
  204. package/src/lib/framework/form-engine/value-transformers.ts +8 -1
  205. package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
  206. package/src/lib/framework/layout-engine/page-layout.tsx +69 -57
  207. package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
  208. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
  209. package/src/lib/framework/page/detail-page.stories.tsx +151 -0
  210. package/src/lib/framework/page/detail-page.tsx +12 -15
  211. package/src/lib/framework/page/list-page.stories.tsx +217 -0
  212. package/src/lib/framework/page/list-page.tsx +8 -1
  213. package/src/lib/graphql/api.ts +18 -1
  214. package/src/lib/graphql/graphql-env.d.ts +1 -1
  215. package/src/lib/hooks/use-alerts.ts +84 -0
  216. package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
  217. package/src/lib/index.ts +12 -5
  218. package/src/lib/providers/alerts-provider.tsx +60 -0
  219. package/src/lib/providers/channel-provider.tsx +1 -0
  220. package/src/lib/providers/theme-provider.tsx +6 -3
@@ -10,7 +10,7 @@ import {
10
10
  import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
11
11
  import { api } from '@/vdb/graphql/api.js';
12
12
  import { graphql } from '@/vdb/graphql/graphql.js';
13
- import { Trans } from '@lingui/react/macro';
13
+ import { Trans, useLingui } from '@lingui/react/macro';
14
14
  import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
15
15
  import { useDebounce } from '@uidotdev/usehooks';
16
16
  import { ChevronRight, Loader2, Plus } from 'lucide-react';
@@ -141,21 +141,24 @@ const getFacetValuesForFacetDocument = graphql(`
141
141
  * @since 3.4.0
142
142
  */
143
143
  export function FacetValueSelector({
144
- onValueSelect,
145
- disabled,
146
- placeholder = 'Search facet values...',
147
- pageSize = 4,
148
- }: FacetValueSelectorProps) {
144
+ onValueSelect,
145
+ disabled,
146
+ placeholder,
147
+ pageSize = 10,
148
+ }: FacetValueSelectorProps) {
149
149
  const [open, setOpen] = useState(false);
150
150
  const [searchTerm, setSearchTerm] = useState('');
151
151
  const [expandedFacetId, setExpandedFacetId] = useState<string | null>(null);
152
+ const [browseMode, setBrowseMode] = useState(false);
152
153
  const debouncedSearch = useDebounce(searchTerm, 200);
154
+ const { t } = useLingui();
155
+ const minSearchLength = 1;
153
156
 
154
157
  // Query for facet values based on search
155
158
  const { data: facetValueData, isLoading: isLoadingFacetValues } = useQuery({
156
159
  queryKey: ['facetValues', debouncedSearch],
157
160
  queryFn: () => {
158
- if (debouncedSearch.length < 2) {
161
+ if (debouncedSearch.length < minSearchLength) {
159
162
  return { facetValues: { items: [], totalItems: 0 } };
160
163
  }
161
164
  return api.query(getFacetValueListDocument, {
@@ -167,14 +170,14 @@ export function FacetValueSelector({
167
170
  },
168
171
  });
169
172
  },
170
- enabled: debouncedSearch.length >= 2 && !expandedFacetId,
173
+ enabled: debouncedSearch.length >= minSearchLength && !expandedFacetId,
171
174
  });
172
175
 
173
- // Query for facets based on search
174
- const { data: facetData, isLoading: isLoadingFacets } = useQuery({
176
+ // Query for facets based on search (use regular query for search, infinite for browse)
177
+ const { data: facetSearchData, isLoading: isLoadingFacetSearch } = useQuery({
175
178
  queryKey: ['facets', debouncedSearch],
176
179
  queryFn: () => {
177
- if (debouncedSearch.length < 2) {
180
+ if (debouncedSearch.length < minSearchLength) {
178
181
  return { facets: { items: [], totalItems: 0 } };
179
182
  }
180
183
  return api.query(getFacetListDocument, {
@@ -186,7 +189,36 @@ export function FacetValueSelector({
186
189
  },
187
190
  });
188
191
  },
189
- enabled: debouncedSearch.length >= 2 && !expandedFacetId,
192
+ enabled: !browseMode && debouncedSearch.length >= minSearchLength && !expandedFacetId,
193
+ });
194
+
195
+ // Infinite query for browse mode
196
+ const {
197
+ data: facetBrowseData,
198
+ isLoading: isLoadingFacetBrowse,
199
+ fetchNextPage: fetchNextFacetsPage,
200
+ hasNextPage: hasNextFacetsPage,
201
+ isFetchingNextPage: isFetchingNextFacetsPage,
202
+ } = useInfiniteQuery({
203
+ queryKey: ['facets', 'browse'],
204
+ queryFn: async ({ pageParam = 0 }) => {
205
+ const response = await api.query(getFacetListDocument, {
206
+ options: {
207
+ filter: {},
208
+ sort: { name: 'ASC' },
209
+ skip: pageParam * pageSize,
210
+ take: pageSize,
211
+ },
212
+ });
213
+ return response.facets;
214
+ },
215
+ getNextPageParam: (lastPage, allPages) => {
216
+ if (!lastPage) return undefined;
217
+ const totalFetched = allPages.length * pageSize;
218
+ return totalFetched < lastPage.totalItems ? allPages.length : undefined;
219
+ },
220
+ enabled: browseMode && !expandedFacetId,
221
+ initialPageParam: 0,
190
222
  });
191
223
 
192
224
  // Query for paginated values of a specific facet when expanded
@@ -220,7 +252,9 @@ export function FacetValueSelector({
220
252
  });
221
253
 
222
254
  const facetValues = facetValueData?.facetValues.items ?? [];
223
- const facets = facetData?.facets.items ?? [];
255
+ const facets = browseMode
256
+ ? (facetBrowseData?.pages.flatMap(page => page?.items ?? []) ?? [])
257
+ : (facetSearchData?.facets.items ?? []);
224
258
  const expandedFacetValues = expandedFacetData?.pages.flatMap(page => page?.items ?? []) ?? [];
225
259
  const expandedFacetName = expandedFacetValues[0]?.facet.name;
226
260
 
@@ -237,14 +271,22 @@ export function FacetValueSelector({
237
271
  {},
238
272
  );
239
273
 
240
- const isLoading = isLoadingFacetValues || isLoadingFacets || isLoadingExpandedFacet;
274
+ const isLoading =
275
+ isLoadingFacetValues || isLoadingFacetSearch || isLoadingFacetBrowse || isLoadingExpandedFacet;
241
276
 
242
277
  const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
243
278
  const target = e.currentTarget;
244
279
  const scrolledToBottom = Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 1;
245
280
 
246
- if (scrolledToBottom && hasNextPage && !isFetchingNextPage) {
247
- fetchNextPage();
281
+ if (scrolledToBottom && !isFetchingNextPage) {
282
+ // For expanded facet values
283
+ if (expandedFacetId && hasNextPage) {
284
+ void fetchNextPage();
285
+ }
286
+ // For browse mode facets
287
+ if (browseMode && !expandedFacetId && hasNextFacetsPage) {
288
+ void fetchNextFacetsPage();
289
+ }
248
290
  }
249
291
  };
250
292
 
@@ -259,22 +301,59 @@ export function FacetValueSelector({
259
301
  <PopoverContent className="p-0 w-[400px]" align="start">
260
302
  <Command shouldFilter={false}>
261
303
  <CommandInput
262
- placeholder={placeholder}
304
+ placeholder={placeholder ?? t`Search facet values...`}
263
305
  value={searchTerm}
264
306
  onValueChange={value => {
265
307
  setSearchTerm(value);
266
308
  setExpandedFacetId(null);
309
+ setBrowseMode(false);
310
+ }}
311
+ onKeyDown={e => {
312
+ if (
313
+ e.key === 'ArrowDown' &&
314
+ !browseMode &&
315
+ debouncedSearch.length < minSearchLength
316
+ ) {
317
+ e.preventDefault();
318
+ setBrowseMode(true);
319
+ }
267
320
  }}
268
321
  disabled={disabled}
269
322
  />
270
323
  <CommandList className="h-[200px] overflow-y-auto" onScroll={handleScroll}>
271
324
  <CommandEmpty>
272
- {debouncedSearch.length < 2 ? (
273
- <Trans>Type at least 2 characters to search...</Trans>
325
+ {debouncedSearch.length < 2 && !browseMode ? (
326
+ <div className="flex flex-col items-center gap-2 py-4">
327
+ <div className="text-sm text-muted-foreground">
328
+ <Trans>Type at least 2 characters to search...</Trans>
329
+ </div>
330
+ <Button
331
+ variant="outline"
332
+ size="sm"
333
+ onClick={() => setBrowseMode(true)}
334
+ type="button"
335
+ >
336
+ <Trans>Browse facets</Trans>
337
+ </Button>
338
+ </div>
274
339
  ) : isLoading ? (
275
340
  <Trans>Loading...</Trans>
276
341
  ) : (
277
- <Trans>No results found</Trans>
342
+ <div className="flex flex-col items-center gap-2 py-4">
343
+ <div className="text-sm text-muted-foreground">
344
+ <Trans>No results found</Trans>
345
+ </div>
346
+ {!browseMode && (
347
+ <Button
348
+ variant="outline"
349
+ size="sm"
350
+ onClick={() => setBrowseMode(true)}
351
+ type="button"
352
+ >
353
+ <Trans>Browse facets</Trans>
354
+ </Button>
355
+ )}
356
+ </div>
278
357
  )}
279
358
  </CommandEmpty>
280
359
 
@@ -282,7 +361,10 @@ export function FacetValueSelector({
282
361
  <>
283
362
  <CommandGroup>
284
363
  <CommandItem
285
- onSelect={() => setExpandedFacetId(null)}
364
+ onSelect={() => {
365
+ setExpandedFacetId(null);
366
+ setBrowseMode(false);
367
+ }}
286
368
  className="cursor-pointer"
287
369
  >
288
370
  ← <Trans>Back to search</Trans>
@@ -297,6 +379,7 @@ export function FacetValueSelector({
297
379
  onValueSelect(facetValue);
298
380
  setSearchTerm('');
299
381
  setExpandedFacetId(null);
382
+ setBrowseMode(false);
300
383
  setOpen(false);
301
384
  }}
302
385
  >
@@ -318,19 +401,31 @@ export function FacetValueSelector({
318
401
  ) : (
319
402
  <>
320
403
  {facets.length > 0 && (
321
- <CommandGroup heading={<Trans>Facets</Trans>}>
322
- {facets.map(facet => (
323
- <CommandItem
324
- key={facet.id}
325
- value={`facet-${facet.id}`}
326
- onSelect={() => setExpandedFacetId(facet.id)}
327
- className="cursor-pointer"
328
- >
329
- <span className="flex-1">{facet.name}</span>
330
- <ChevronRight className="h-4 w-4" />
331
- </CommandItem>
332
- ))}
333
- </CommandGroup>
404
+ <>
405
+ <CommandGroup heading={<Trans>Facets</Trans>}>
406
+ {facets.map(facet => (
407
+ <CommandItem
408
+ key={facet.id}
409
+ value={`facet-${facet.id}`}
410
+ onSelect={() => setExpandedFacetId(facet.id)}
411
+ className="cursor-pointer"
412
+ >
413
+ <span className="flex-1">{facet.name}</span>
414
+ <ChevronRight className="h-4 w-4" />
415
+ </CommandItem>
416
+ ))}
417
+ </CommandGroup>
418
+ {browseMode && isFetchingNextFacetsPage && (
419
+ <div className="flex items-center justify-center py-2">
420
+ <Loader2 className="h-4 w-4 animate-spin" />
421
+ </div>
422
+ )}
423
+ {browseMode && !hasNextFacetsPage && facets.length > 0 && (
424
+ <div className="text-center py-2 text-sm text-muted-foreground">
425
+ <Trans>No more facets</Trans>
426
+ </div>
427
+ )}
428
+ </>
334
429
  )}
335
430
 
336
431
  {Object.entries(facetGroups).map(
@@ -343,6 +438,7 @@ export function FacetValueSelector({
343
438
  onSelect={() => {
344
439
  onValueSelect(facetValue);
345
440
  setSearchTerm('');
441
+ setBrowseMode(false);
346
442
  setOpen(false);
347
443
  }}
348
444
  >
@@ -0,0 +1,212 @@
1
+ import { Money } from '@/vdb/components/data-display/money.js';
2
+ import { Badge } from '@/vdb/components/ui/badge.js';
3
+ import { FullWidthPageBlock, Page, PageLayout } from '@/vdb/framework/layout-engine/page-layout.js';
4
+ import { graphql } from '@/vdb/graphql/graphql.js';
5
+ import type { Meta, StoryObj } from '@storybook/react-vite';
6
+ import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
7
+ import { useState } from 'react';
8
+ import { withDescription } from '../../../../.storybook/with-description.js';
9
+ import { PaginatedListDataTable } from './paginated-list-data-table.js';
10
+
11
+ // GraphQL query for products list
12
+ const productsListDocument = graphql(`
13
+ query ProductsList($options: ProductListOptions) {
14
+ products(options: $options) {
15
+ items {
16
+ id
17
+ createdAt
18
+ updatedAt
19
+ name
20
+ slug
21
+ description
22
+ enabled
23
+ featuredAsset {
24
+ id
25
+ preview
26
+ }
27
+ variants {
28
+ id
29
+ name
30
+ sku
31
+ price
32
+ priceWithTax
33
+ currencyCode
34
+ }
35
+ }
36
+ totalItems
37
+ }
38
+ }
39
+ `);
40
+
41
+ const meta = {
42
+ title: 'Components/PaginatedListDataTable',
43
+ component: PaginatedListDataTable,
44
+ ...withDescription(import.meta.url, './paginated-list-data-table.js'),
45
+ parameters: {
46
+ layout: 'fullscreen',
47
+ },
48
+ tags: ['autodocs'],
49
+ } satisfies Meta<typeof PaginatedListDataTable>;
50
+
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ export const Playground: Story = {
55
+ render: () => {
56
+ const [page, setPage] = useState(1);
57
+ const [pageSize, setPageSize] = useState(10);
58
+ const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }]);
59
+ const [filters, setFilters] = useState<ColumnFiltersState>([]);
60
+
61
+ return (
62
+ <div className="p-6">
63
+ <Page pageId="test-page">
64
+ <PageLayout>
65
+ <FullWidthPageBlock blockId="test-block">
66
+ <PaginatedListDataTable
67
+ listQuery={productsListDocument}
68
+ defaultVisibility={{
69
+ id: false,
70
+ updatedAt: false,
71
+ description: false,
72
+ slug: false,
73
+ variants: false,
74
+ }}
75
+ customizeColumns={{
76
+ name: {
77
+ header: 'Product Name',
78
+ cell: ({ cell, row }) => {
79
+ const value = cell.getValue() as string;
80
+ return (
81
+ <div className="flex items-center gap-2">
82
+ {row.original.featuredAsset && (
83
+ <img
84
+ src={row.original.featuredAsset.preview}
85
+ alt={value}
86
+ className="w-8 h-8 rounded object-cover"
87
+ />
88
+ )}
89
+ <span className="font-medium">{value}</span>
90
+ </div>
91
+ );
92
+ },
93
+ },
94
+ enabled: {
95
+ header: 'Status',
96
+ cell: ({ cell }) => {
97
+ const value = cell.getValue() as boolean;
98
+ return (
99
+ <Badge variant={value ? 'default' : 'secondary'}>
100
+ {value ? 'Enabled' : 'Disabled'}
101
+ </Badge>
102
+ );
103
+ },
104
+ },
105
+ slug: {
106
+ header: 'Slug',
107
+ cell: ({ cell }) => {
108
+ const value = cell.getValue() as string;
109
+ return (
110
+ <code className="text-xs bg-muted px-1 py-0.5 rounded">
111
+ {value}
112
+ </code>
113
+ );
114
+ },
115
+ },
116
+ }}
117
+ additionalColumns={{
118
+ variantCount: {
119
+ header: 'Variants',
120
+ accessorFn: row => row.variants?.length ?? 0,
121
+ cell: ({ cell }) => {
122
+ const count = cell.getValue() as number;
123
+ return <span className="text-muted-foreground">{count}</span>;
124
+ },
125
+ },
126
+ price: {
127
+ header: 'Price Range',
128
+ accessorFn: row => {
129
+ const variants = row.variants ?? [];
130
+ if (variants.length === 0) return null;
131
+ const prices = variants.map(v => v.price);
132
+ const min = Math.min(...prices);
133
+ const max = Math.max(...prices);
134
+ return { min, max, currency: variants[0].currencyCode };
135
+ },
136
+ cell: ({ cell }) => {
137
+ const value = cell.getValue() as {
138
+ min: number;
139
+ max: number;
140
+ currency: string;
141
+ } | null;
142
+ if (!value) return null;
143
+ if (value.min === value.max) {
144
+ return <Money value={value.min} currency={value.currency} />;
145
+ }
146
+ return (
147
+ <span>
148
+ <Money value={value.min} currency={value.currency} /> -{' '}
149
+ <Money value={value.max} currency={value.currency} />
150
+ </span>
151
+ );
152
+ },
153
+ meta: {
154
+ dependencies: ['variants'],
155
+ },
156
+ },
157
+ }}
158
+ defaultColumnOrder={[
159
+ 'name',
160
+ 'slug',
161
+ 'enabled',
162
+ 'variantCount',
163
+ 'price',
164
+ 'createdAt',
165
+ ]}
166
+ onSearchTermChange={searchTerm => {
167
+ if (!searchTerm) return {};
168
+ return {
169
+ name: { contains: searchTerm },
170
+ };
171
+ }}
172
+ page={page}
173
+ itemsPerPage={pageSize}
174
+ sorting={sorting}
175
+ columnFilters={filters}
176
+ onPageChange={(_, page, perPage) => {
177
+ setPage(page);
178
+ setPageSize(perPage);
179
+ }}
180
+ onSortChange={(_, sorting) => {
181
+ setSorting(sorting);
182
+ }}
183
+ onFilterChange={(_, filters) => {
184
+ setFilters(filters);
185
+ }}
186
+ facetedFilters={{
187
+ enabled: {
188
+ title: 'Status',
189
+ options: [
190
+ { label: 'Enabled', value: true },
191
+ { label: 'Disabled', value: false },
192
+ ],
193
+ },
194
+ }}
195
+ rowActions={[
196
+ {
197
+ label: 'Edit',
198
+ onClick: row => console.log('Edit product:', row.original.id),
199
+ },
200
+ {
201
+ label: 'Duplicate',
202
+ onClick: row => console.log('Duplicate product:', row.original.id),
203
+ },
204
+ ]}
205
+ />
206
+ </FullWidthPageBlock>
207
+ </PageLayout>
208
+ </Page>
209
+ </div>
210
+ );
211
+ },
212
+ };
@@ -394,17 +394,6 @@ export function PaginatedListDataTable<
394
394
  return { ...acc, [field]: direction };
395
395
  }, {});
396
396
 
397
- const filter = columnFilters?.length
398
- ? {
399
- _and: columnFilters.map(f => {
400
- if (Array.isArray(f.value)) {
401
- return { [f.id]: { in: f.value } };
402
- }
403
- return { [f.id]: f.value };
404
- }),
405
- }
406
- : undefined;
407
-
408
397
  function refetchPaginatedList() {
409
398
  queryClient.invalidateQueries({ queryKey });
410
399
  }
@@ -438,6 +427,17 @@ export function PaginatedListDataTable<
438
427
  }));
439
428
  const minimalListQuery = includeOnlySelectedListFields(extendedListQuery, visibleColumns);
440
429
 
430
+ const filter = columnFilters?.length
431
+ ? {
432
+ _and: columnFilters.map(f => {
433
+ if (Array.isArray(f.value)) {
434
+ return { [f.id]: { in: f.value } };
435
+ }
436
+ return { [f.id]: f.value };
437
+ }),
438
+ }
439
+ : undefined;
440
+
441
441
  const defaultQueryKey = [
442
442
  PaginatedListDataTableKey,
443
443
  minimalListQuery,
@@ -456,7 +456,7 @@ export function PaginatedListDataTable<
456
456
  const mergedFilter = { ...filter, ...searchFilter };
457
457
  const variables = {
458
458
  options: {
459
- take: itemsPerPage,
459
+ take: Math.min(itemsPerPage, 100),
460
460
  skip: (page - 1) * itemsPerPage,
461
461
  sort,
462
462
  filter: mergedFilter,
@@ -0,0 +1,46 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { withDescription } from '../../../../.storybook/with-description.js';
4
+ import { PermissionGuard } from './permission-guard.js';
5
+
6
+ const meta = {
7
+ title: 'Components/PermissionGuard',
8
+ component: PermissionGuard,
9
+ ...withDescription(import.meta.url, './permission-guard.js'),
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ } satisfies Meta<typeof PermissionGuard>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Playground: Story = {
20
+ render: () => {
21
+ return (
22
+ <div className="space-y-6">
23
+ <div className="space-y-2">
24
+ <div className="text-sm font-medium">
25
+ Has Permission (ReadCatalog) - Button should be visible
26
+ </div>
27
+ <PermissionGuard requires={['ReadCatalog']}>
28
+ <Button>View Catalog</Button>
29
+ </PermissionGuard>
30
+ </div>
31
+
32
+ <div className="space-y-2">
33
+ <div className="text-sm font-medium">
34
+ No Permission (NonExistentPermission) - Button should be hidden
35
+ </div>
36
+ <PermissionGuard requires={['NonExistentPermission']}>
37
+ <Button variant="destructive">Delete Catalog</Button>
38
+ </PermissionGuard>
39
+ <div className="text-xs text-muted-foreground">
40
+ (The delete button is hidden because user lacks NonExistentPermission permission)
41
+ </div>
42
+ </div>
43
+ </div>
44
+ );
45
+ },
46
+ };
@@ -4,6 +4,7 @@ import { toast } from 'sonner';
4
4
 
5
5
  import { DataTableBulkActionItem } from '@/vdb/components/data-table/data-table-bulk-action-item.js';
6
6
  import { usePaginatedList } from '@/vdb/components/shared/paginated-list-data-table.js';
7
+ import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
7
8
  import { ResultOf } from '@/vdb/graphql/graphql.js';
8
9
  import { useChannel } from '@/vdb/hooks/use-channel.js';
9
10
  import { Trans, useLingui } from '@lingui/react/macro';
@@ -86,6 +87,7 @@ export function RemoveFromChannelBulkAction({
86
87
  }
87
88
  icon={LayersIcon}
88
89
  className="text-warning"
90
+ disabled={activeChannel.code === DEFAULT_CHANNEL_CODE}
89
91
  />
90
92
  );
91
93
  }
@@ -83,6 +83,10 @@ export function ResponsiveToolbar({ editor, disabled }: Readonly<ResponsiveToolb
83
83
  [],
84
84
  );
85
85
 
86
+ const canUndo = !!editor?.can().undo();
87
+ const canRedo = !!editor?.can().redo();
88
+ const canInsertTable = !!editor?.can().insertTable();
89
+
86
90
  const toolbarItems: ToolbarItem[] = useMemo(() => {
87
91
  if (!editor) return [];
88
92
 
@@ -272,7 +276,7 @@ export function ResponsiveToolbar({ editor, disabled }: Readonly<ResponsiveToolb
272
276
  .run()
273
277
  }
274
278
  className={`h-8 px-2 ${editor.isActive('table') ? 'bg-accent' : ''}`}
275
- disabled={disabled || !editor.can().insertTable()}
279
+ disabled={disabled || !canInsertTable}
276
280
  >
277
281
  <TableIcon className="h-4 w-4" />
278
282
  </Button>
@@ -290,7 +294,7 @@ export function ResponsiveToolbar({ editor, disabled }: Readonly<ResponsiveToolb
290
294
  variant="ghost"
291
295
  size="sm"
292
296
  onClick={() => editor.chain().focus().undo().run()}
293
- disabled={disabled || !editor.can().undo()}
297
+ disabled={disabled || !canUndo}
294
298
  className="h-8 px-2"
295
299
  >
296
300
  <Undo2Icon className="h-4 w-4" />
@@ -309,7 +313,7 @@ export function ResponsiveToolbar({ editor, disabled }: Readonly<ResponsiveToolb
309
313
  variant="ghost"
310
314
  size="sm"
311
315
  onClick={() => editor.chain().focus().redo().run()}
312
- disabled={disabled || !editor.can().redo()}
316
+ disabled={disabled || !canRedo}
313
317
  className="h-8 px-2"
314
318
  >
315
319
  <Redo2Icon className="h-4 w-4" />
@@ -317,7 +321,7 @@ export function ResponsiveToolbar({ editor, disabled }: Readonly<ResponsiveToolb
317
321
  ),
318
322
  },
319
323
  ];
320
- }, [editor, disabled, linkDialogOpen, imageDialogOpen]);
324
+ }, [editor, disabled, linkDialogOpen, imageDialogOpen, canUndo, canRedo, canInsertTable]);
321
325
 
322
326
  useEffect(() => {
323
327
  const calculateVisibleItems = () => {
@@ -5,6 +5,7 @@ import { TextStyle } from '@tiptap/extension-text-style';
5
5
  import { EditorContent, useEditor } from '@tiptap/react';
6
6
  import StarterKit from '@tiptap/starter-kit';
7
7
  import { useLayoutEffect, useRef } from 'react';
8
+
8
9
  import { ResponsiveToolbar } from './responsive-toolbar.js';
9
10
  import { TableDeleteMenu } from './table-delete-menu.js';
10
11
  import { TableEditIcons } from './table-edit-icons.js';