@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
@@ -0,0 +1,249 @@
1
+ import { Badge } from '@/vdb/components/ui/badge.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import { FullWidthPageBlock, Page, PageLayout } from '@/vdb/framework/layout-engine/page-layout.js';
4
+ import type { Meta, StoryObj } from '@storybook/react-vite';
5
+ import { ColumnDef, ColumnFiltersState, SortingState } from '@tanstack/react-table';
6
+ import { useState } from 'react';
7
+ import { withDescription } from '../../../../.storybook/with-description.js';
8
+ import { DataTable } from './data-table.js';
9
+
10
+ // Sample data type
11
+ interface Product {
12
+ id: string;
13
+ name: string;
14
+ category: string;
15
+ price: number;
16
+ stock: number;
17
+ status: 'active' | 'inactive' | 'discontinued';
18
+ createdAt: string;
19
+ }
20
+
21
+ // Sample data
22
+ const sampleData: Product[] = Array.from({ length: 100 }, (_, i) => ({
23
+ id: `product-${i + 1}`,
24
+ name: `Product ${i + 1}`,
25
+ category: ['Electronics', 'Clothing', 'Home & Garden', 'Sports', 'Books'][i % 5],
26
+ price: Math.floor(Math.random() * 1000) + 10,
27
+ stock: Math.floor(Math.random() * 100),
28
+ status: (['active', 'inactive', 'discontinued'] as const)[i % 3],
29
+ createdAt: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
30
+ }));
31
+
32
+ const meta = {
33
+ title: 'Components/DataTable',
34
+ component: DataTable,
35
+ ...withDescription(import.meta.url, './data-table.js'),
36
+ parameters: {
37
+ layout: 'fullscreen',
38
+ },
39
+ tags: ['autodocs'],
40
+ } satisfies Meta<typeof DataTable>;
41
+
42
+ export default meta;
43
+ type Story = StoryObj<typeof meta>;
44
+
45
+ export const Playground: Story = {
46
+ render: () => {
47
+ const [page, setPage] = useState(1);
48
+ const [pageSize, setPageSize] = useState(10);
49
+ const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }]);
50
+ const [filters, setFilters] = useState<ColumnFiltersState>([]);
51
+ const [searchTerm, setSearchTerm] = useState('');
52
+
53
+ // Define columns
54
+ const columns: ColumnDef<Product>[] = [
55
+ {
56
+ id: 'name',
57
+ accessorKey: 'name',
58
+ header: 'Product Name',
59
+ cell: ({ row }) => <span className="font-medium">{row.original.name}</span>,
60
+ },
61
+ {
62
+ id: 'category',
63
+ accessorKey: 'category',
64
+ header: 'Category',
65
+ cell: ({ row }) => <span>{row.original.category}</span>,
66
+ },
67
+ {
68
+ id: 'price',
69
+ accessorKey: 'price',
70
+ header: 'Price',
71
+ cell: ({ row }) => <span>${row.original.price.toFixed(2)}</span>,
72
+ },
73
+ {
74
+ id: 'stock',
75
+ accessorKey: 'stock',
76
+ header: 'Stock',
77
+ cell: ({ row }) => {
78
+ const stock = row.original.stock;
79
+ return (
80
+ <Badge variant={stock > 50 ? 'default' : stock > 20 ? 'secondary' : 'destructive'}>
81
+ {stock}
82
+ </Badge>
83
+ );
84
+ },
85
+ },
86
+ {
87
+ id: 'status',
88
+ accessorKey: 'status',
89
+ header: 'Status',
90
+ cell: ({ row }) => {
91
+ const status = row.original.status;
92
+ return (
93
+ <Badge
94
+ variant={
95
+ status === 'active'
96
+ ? 'default'
97
+ : status === 'inactive'
98
+ ? 'secondary'
99
+ : 'outline'
100
+ }
101
+ >
102
+ {status}
103
+ </Badge>
104
+ );
105
+ },
106
+ },
107
+ {
108
+ id: 'createdAt',
109
+ accessorKey: 'createdAt',
110
+ header: 'Created',
111
+ cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
112
+ },
113
+ {
114
+ id: 'actions',
115
+ header: 'Actions',
116
+ cell: ({ row }) => (
117
+ <div className="flex gap-2">
118
+ <Button
119
+ variant="ghost"
120
+ size="sm"
121
+ onClick={() => console.log('Edit', row.original.id)}
122
+ >
123
+ Edit
124
+ </Button>
125
+ <Button
126
+ variant="ghost"
127
+ size="sm"
128
+ onClick={() => console.log('Delete', row.original.id)}
129
+ >
130
+ Delete
131
+ </Button>
132
+ </div>
133
+ ),
134
+ },
135
+ ];
136
+
137
+ // Filter and sort data based on state
138
+ let filteredData = [...sampleData];
139
+
140
+ // Apply search filter
141
+ if (searchTerm) {
142
+ filteredData = filteredData.filter(item =>
143
+ item.name.toLowerCase().includes(searchTerm.toLowerCase()),
144
+ );
145
+ }
146
+
147
+ // Apply column filters
148
+ filters.forEach(filter => {
149
+ if (filter.id === 'status' && Array.isArray(filter.value)) {
150
+ filteredData = filteredData.filter(item => filter.value.includes(item.status));
151
+ }
152
+ if (filter.id === 'category' && Array.isArray(filter.value)) {
153
+ filteredData = filteredData.filter(item => filter.value.includes(item.category));
154
+ }
155
+ });
156
+
157
+ // Apply sorting
158
+ if (sorting.length > 0) {
159
+ const sort = sorting[0];
160
+ filteredData.sort((a, b) => {
161
+ const aValue = a[sort.id as keyof Product];
162
+ const bValue = b[sort.id as keyof Product];
163
+ if (aValue < bValue) return sort.desc ? 1 : -1;
164
+ if (aValue > bValue) return sort.desc ? -1 : 1;
165
+ return 0;
166
+ });
167
+ }
168
+
169
+ const totalItems = filteredData.length;
170
+ const paginatedData = filteredData.slice((page - 1) * pageSize, page * pageSize);
171
+
172
+ return (
173
+ <div className="p-6">
174
+ <Page pageId="test-page">
175
+ <PageLayout>
176
+ <FullWidthPageBlock blockId="test-block">
177
+ <DataTable
178
+ columns={columns}
179
+ data={paginatedData}
180
+ totalItems={totalItems}
181
+ page={page}
182
+ itemsPerPage={pageSize}
183
+ sorting={sorting}
184
+ columnFilters={filters}
185
+ defaultColumnVisibility={{
186
+ createdAt: false,
187
+ }}
188
+ onPageChange={(_, newPage, newPageSize) => {
189
+ setPage(newPage);
190
+ setPageSize(newPageSize);
191
+ }}
192
+ onSortChange={(_, newSorting) => {
193
+ setSorting(newSorting);
194
+ }}
195
+ onFilterChange={(_, newFilters) => {
196
+ setFilters(newFilters);
197
+ setPage(1); // Reset to first page when filters change
198
+ }}
199
+ onSearchTermChange={term => {
200
+ setSearchTerm(term);
201
+ setPage(1); // Reset to first page when search changes
202
+ }}
203
+ facetedFilters={{
204
+ status: {
205
+ title: 'Status',
206
+ options: [
207
+ { label: 'Active', value: 'active' },
208
+ { label: 'Inactive', value: 'inactive' },
209
+ { label: 'Discontinued', value: 'discontinued' },
210
+ ],
211
+ },
212
+ category: {
213
+ title: 'Category',
214
+ options: [
215
+ { label: 'Electronics', value: 'Electronics' },
216
+ { label: 'Clothing', value: 'Clothing' },
217
+ { label: 'Home & Garden', value: 'Home & Garden' },
218
+ { label: 'Sports', value: 'Sports' },
219
+ { label: 'Books', value: 'Books' },
220
+ ],
221
+ },
222
+ }}
223
+ bulkActions={[
224
+ {
225
+ label: 'Delete selected',
226
+ icon: 'trash',
227
+ onClick: (selectedItems: Product[]) => {
228
+ console.log('Delete selected:', selectedItems);
229
+ },
230
+ },
231
+ {
232
+ label: 'Export selected',
233
+ icon: 'download',
234
+ onClick: (selectedItems: Product[]) => {
235
+ console.log('Export selected:', selectedItems);
236
+ },
237
+ },
238
+ ]}
239
+ onRefresh={() => {
240
+ console.log('Refresh data');
241
+ }}
242
+ />
243
+ </FullWidthPageBlock>
244
+ </PageLayout>
245
+ </Page>
246
+ </div>
247
+ );
248
+ },
249
+ };
@@ -29,12 +29,12 @@ import {
29
29
  VisibilityState,
30
30
  } from '@tanstack/react-table';
31
31
  import { RowSelectionState, TableOptions } from '@tanstack/table-core';
32
- import React, { Suspense, useEffect } from 'react';
32
+ import React, { Suspense, useEffect, useRef } from 'react';
33
33
  import { AddFilterMenu } from './add-filter-menu.js';
34
34
  import { DataTableBulkActions } from './data-table-bulk-actions.js';
35
35
  import { DataTableProvider } from './data-table-context.js';
36
36
  import { DataTableFacetedFilter, DataTableFacetedFilterOption } from './data-table-faceted-filter.js';
37
- import { DataTableFilterBadge } from './data-table-filter-badge.js';
37
+ import { DataTableFilterBadgeEditable } from './data-table-filter-badge-editable.js';
38
38
 
39
39
  export interface FacetedFilter {
40
40
  title: string;
@@ -128,6 +128,8 @@ export function DataTable<TData>({
128
128
  defaultColumnVisibility ?? {},
129
129
  );
130
130
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});
131
+ const prevSearchTermRef = useRef(searchTerm);
132
+ const prevColumnFiltersRef = useRef(columnFilters);
131
133
 
132
134
  useEffect(() => {
133
135
  // If the defaultColumnVisibility changes externally (e.g. the user reset the table settings),
@@ -179,21 +181,46 @@ export function DataTable<TData>({
179
181
  onSortChange?.(table, sorting);
180
182
  }, [sorting]);
181
183
 
182
- useEffect(() => {
183
- onFilterChange?.(table, columnFilters);
184
- }, [columnFilters]);
185
-
186
184
  useEffect(() => {
187
185
  onColumnVisibilityChange?.(table, columnVisibility);
188
186
  }, [columnVisibility]);
189
187
 
190
- const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
188
+ useEffect(() => {
189
+ if (page && page > 1 && itemsPerPage && prevSearchTermRef.current !== searchTerm) {
190
+ // Set the page back to 1 when searchTerm changes
191
+ setPagination({
192
+ ...pagination,
193
+ pageIndex: 0,
194
+ });
195
+ }
196
+ prevSearchTermRef.current = searchTerm;
197
+ }, [onPageChange, searchTerm]);
198
+
199
+ useEffect(() => {
200
+ onFilterChange?.(table, columnFilters);
201
+ if (
202
+ page &&
203
+ page > 1 &&
204
+ itemsPerPage &&
205
+ JSON.stringify(prevColumnFiltersRef.current) !== JSON.stringify(columnFilters)
206
+ ) {
207
+ // Set the page back to 1 when filters change
208
+ setPagination({
209
+ ...pagination,
210
+ pageIndex: 0,
211
+ });
212
+ pagination.pageIndex;
213
+ }
214
+ prevColumnFiltersRef.current = columnFilters;
215
+ }, [columnFilters]);
191
216
 
192
217
  const handleSearchChange = (value: string) => {
193
218
  setSearchTerm(value);
194
219
  onSearchTermChange?.(value);
195
220
  };
196
221
 
222
+ const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
223
+
197
224
  return (
198
225
  <DataTableProvider
199
226
  columnFilters={columnFilters}
@@ -243,20 +270,21 @@ export function DataTable<TData>({
243
270
 
244
271
  {(pageId && onFilterChange && globalViews.length > 0) ||
245
272
  columnFilters.filter(f => !facetedFilters?.[f.id]).length > 0 ? (
246
- <div className="flex items-center justify-between bg-muted/40 rounded border border-border p-2">
273
+ <div className="flex items-center justify-between bg-muted/40 rounded border border-border p-2 @container">
247
274
  <div className="flex items-center">
248
275
  {pageId && onFilterChange && <GlobalViewsBar />}
249
276
  </div>
250
- <div className="flex gap-1 items-center">
277
+ <div className="flex gap-1 flex-wrap items-center">
251
278
  {columnFilters
252
279
  .filter(f => !facetedFilters?.[f.id])
253
280
  .map(f => {
254
281
  const column = table.getColumn(f.id);
255
282
  const currency = activeChannel?.defaultCurrencyCode ?? 'USD';
256
283
  return (
257
- <DataTableFilterBadge
284
+ <DataTableFilterBadgeEditable
258
285
  key={f.id}
259
286
  filter={f}
287
+ column={column}
260
288
  currencyCode={currency}
261
289
  dataType={
262
290
  (column?.columnDef.meta as any)?.fieldInfo?.type ?? 'String'
@@ -303,7 +331,7 @@ export function DataTable<TData>({
303
331
  </TableHeader>
304
332
  <TableBody>
305
333
  {isLoading && !data?.length ? (
306
- Array.from({ length: pagination.pageSize }).map((_, index) => (
334
+ Array.from({ length: Math.min(pagination.pageSize, 100) }).map((_, index) => (
307
335
  <TableRow
308
336
  key={`skeleton-${index}`}
309
337
  className="animate-in fade-in duration-100"
@@ -10,47 +10,92 @@ export interface DataTableDateTimeFilterProps {
10
10
 
11
11
  export const DATETIME_OPERATORS = ['eq', 'before', 'after', 'between', 'isNull'] as const;
12
12
 
13
+ export interface DateTimeFilterResult {
14
+ filter: Record<string, any>;
15
+ error?: string;
16
+ }
17
+
18
+ export interface ParsedDateTimeFilter {
19
+ operator: string;
20
+ value?: Date;
21
+ startDate?: Date;
22
+ endDate?: Date;
23
+ }
24
+
25
+ export function parseDateTimeFilter(incomingValue: Record<string, any> | undefined): ParsedDateTimeFilter {
26
+ if (!incomingValue || Object.keys(incomingValue).length === 0) {
27
+ return { operator: 'eq' };
28
+ }
29
+
30
+ const operator = Object.keys(incomingValue)[0];
31
+ const value = Object.values(incomingValue)[0];
32
+
33
+ if (operator === 'isNull') {
34
+ return { operator };
35
+ }
36
+
37
+ if (operator === 'between' && typeof value === 'object' && value !== null) {
38
+ return {
39
+ operator,
40
+ startDate: value.start ? new Date(value.start) : undefined,
41
+ endDate: value.end ? new Date(value.end) : undefined,
42
+ };
43
+ }
44
+
45
+ // For eq, before, after operators
46
+ return {
47
+ operator,
48
+ value: typeof value === 'string' ? new Date(value) : undefined,
49
+ };
50
+ }
51
+
52
+ export function buildDateTimeFilter(
53
+ operator: string,
54
+ value?: Date,
55
+ startDate?: Date,
56
+ endDate?: Date,
57
+ ): DateTimeFilterResult {
58
+ if (operator === 'isNull') {
59
+ return { filter: { [operator]: true } };
60
+ }
61
+
62
+ if (operator === 'between') {
63
+ if (!startDate && !endDate) {
64
+ return { filter: {} };
65
+ }
66
+ if (!startDate || !endDate) {
67
+ return { filter: {}, error: 'Please enter both start and end dates' };
68
+ }
69
+ if (startDate > endDate) {
70
+ return { filter: {}, error: 'Start date must be before end date' };
71
+ }
72
+ return {
73
+ filter: { [operator]: { start: startDate.toISOString(), end: endDate.toISOString() } },
74
+ };
75
+ } else {
76
+ if (!value) {
77
+ return { filter: {} };
78
+ }
79
+ return { filter: { [operator]: value.toISOString() } };
80
+ }
81
+ }
82
+
13
83
  export function DataTableDateTimeFilter({
14
84
  value: incomingValue,
15
85
  onChange,
16
86
  }: Readonly<DataTableDateTimeFilterProps>) {
17
- const initialOperator = incomingValue ? Object.keys(incomingValue)[0] : 'eq';
18
- const initialValue = incomingValue ? Object.values(incomingValue)[0] : '';
19
- const [operator, setOperator] = useState<string>(initialOperator ?? 'eq');
20
- const [value, setValue] = useState<Date | undefined>(initialValue ? new Date(initialValue) : undefined);
21
- const [startDate, setStartDate] = useState<Date | undefined>(undefined);
22
- const [endDate, setEndDate] = useState<Date | undefined>(undefined);
87
+ const parsed = parseDateTimeFilter(incomingValue);
88
+ const [operator, setOperator] = useState<string>(parsed.operator);
89
+ const [value, setValue] = useState<Date | undefined>(parsed.value);
90
+ const [startDate, setStartDate] = useState<Date | undefined>(parsed.startDate);
91
+ const [endDate, setEndDate] = useState<Date | undefined>(parsed.endDate);
23
92
  const [error, setError] = useState<string>('');
24
93
 
25
- useEffect(() => {
26
- if (operator === 'isNull') {
27
- onChange({ [operator]: true });
28
- return;
29
- }
30
94
 
31
- if (operator === 'between') {
32
- if (!startDate && !endDate) {
33
- onChange({});
34
- return;
35
- }
36
- if (!startDate || !endDate) {
37
- setError('Please enter both start and end dates');
38
- return;
39
- }
40
- if (startDate > endDate) {
41
- setError('Start date must be before end date');
42
- return;
43
- }
44
- setError('');
45
- onChange({ [operator]: { start: startDate.toISOString(), end: endDate.toISOString() } });
46
- } else {
47
- if (!value) {
48
- onChange({});
49
- return;
50
- }
51
- setError('');
52
- onChange({ [operator]: value.toISOString() });
53
- }
95
+ useEffect(() => {
96
+ const result = buildDateTimeFilter(operator, value, startDate, endDate);
97
+ onChange(result.filter);
98
+ setError(result.error ?? '');
54
99
  }, [operator, value, startDate, endDate]);
55
100
 
56
101
  const parseToDate = (input: unknown): Date | undefined => {
@@ -5,8 +5,14 @@ import {
5
5
  getOperationVariablesFields,
6
6
  getTypeFieldInfo,
7
7
  } from '@/vdb/framework/document-introspection/get-document-structure.js';
8
+ import {
9
+ generateDisplayComponentKey,
10
+ getDisplayComponent,
11
+ } from '@/vdb/framework/extension-api/display-component-extensions.js';
8
12
  import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
9
13
  import { api } from '@/vdb/graphql/api.js';
14
+ import { usePageBlock } from '@/vdb/hooks/use-page-block.js';
15
+ import { usePage } from '@/vdb/hooks/use-page.js';
10
16
  import { TypedDocumentNode } from '@graphql-typed-document-node/core';
11
17
  import { Trans, useLingui } from '@lingui/react/macro';
12
18
  import { useMutation } from '@tanstack/react-query';
@@ -36,7 +42,12 @@ import {
36
42
  } from '../ui/alert-dialog.js';
37
43
  import { Button } from '../ui/button.js';
38
44
  import { Checkbox } from '../ui/checkbox.js';
39
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../ui/dropdown-menu.js';
45
+ import {
46
+ DropdownMenu,
47
+ DropdownMenuContent,
48
+ DropdownMenuItem,
49
+ DropdownMenuTrigger,
50
+ } from '../ui/dropdown-menu.js';
40
51
  import { DataTableColumnHeader } from './data-table-column-header.js';
41
52
 
42
53
  /**
@@ -77,6 +88,8 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
77
88
  columns: Array<AccessorKeyColumnDef<any> | AccessorFnColumnDef<any>>;
78
89
  customFieldColumnNames: string[];
79
90
  } {
91
+ const { pageId } = usePage();
92
+ const pageBlock = usePageBlock();
80
93
  const columnHelper = createColumnHelper<PaginatedListItemFields<T>>();
81
94
  const allBulkActions = useAllBulkActions(bulkActions ?? []);
82
95
 
@@ -101,47 +114,39 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
101
114
 
102
115
  const queryBasedColumns = columnConfigs.map(({ fieldInfo, isCustomField }) => {
103
116
  const customConfig = customizeColumns?.[fieldInfo.name as unknown as AllItemFieldKeys<T>] ?? {};
104
- const { header, ...customConfigRest } = customConfig;
117
+ const { header, meta, cell: customCell, ...customConfigRest } = customConfig;
105
118
  const enableColumnFilter = fieldInfo.isScalar && !facetedFilters?.[fieldInfo.name];
106
119
 
107
120
  return columnHelper.accessor(fieldInfo.name as any, {
108
121
  id: fieldInfo.name,
109
- meta: { fieldInfo, isCustomField },
122
+ meta: { fieldInfo, isCustomField, ...(meta ?? {}) },
110
123
  enableColumnFilter,
111
- enableSorting: fieldInfo.isScalar && enableSorting,
124
+ enableSorting: fieldInfo.isScalar && fieldInfo.type !== 'Boolean' && enableSorting,
112
125
  // Filtering is done on the server side, but we set this to 'equalsString' because
113
126
  // otherwise the TanStack Table with apply an "auto" function which somehow
114
127
  // prevents certain filters from working.
115
128
  filterFn: 'equalsString',
116
- cell: ({ cell, row }) => {
129
+ cell: cellContext => {
130
+ const { cell, row } = cellContext;
117
131
  const cellValue = cell.getValue();
118
132
  const value =
119
133
  cellValue ??
120
134
  (isCustomField ? row.original?.customFields?.[fieldInfo.name] : undefined);
135
+ const displayComponentId =
136
+ pageId && pageBlock?.blockId
137
+ ? generateDisplayComponentKey(pageId, pageBlock.blockId, fieldInfo.name)
138
+ : undefined;
121
139
 
122
- if (fieldInfo.list && Array.isArray(value)) {
123
- return value.join(', ');
124
- }
125
- if (
126
- (fieldInfo.type === 'DateTime' && typeof value === 'string') ||
127
- value instanceof Date
128
- ) {
129
- return <DisplayComponent id="vendure:dateTime" value={value} />;
130
- }
131
- if (fieldInfo.type === 'Boolean') {
132
- if (cell.column.id === 'enabled') {
133
- return <DisplayComponent id="vendure:booleanBadge" value={value} />;
134
- } else {
135
- return <DisplayComponent id="vendure:booleanCheckbox" value={value} />;
136
- }
137
- }
138
- if (fieldInfo.type === 'Asset') {
139
- return <DisplayComponent id="vendure:asset" value={value} />;
140
+ const CustomDisplayComponent =
141
+ displayComponentId && getDisplayComponent(displayComponentId);
142
+
143
+ if (CustomDisplayComponent) {
144
+ return <CustomDisplayComponent value={value} {...cellContext} />;
140
145
  }
141
- if (value !== null && typeof value === 'object') {
142
- return <DisplayComponent id="vendure:json" value={value} />;
146
+ if (typeof customCell === 'function') {
147
+ return customCell(cellContext);
143
148
  }
144
- return value;
149
+ return <DefaultDisplayComponent value={value} fieldInfo={fieldInfo} />;
145
150
  },
146
151
  header: headerContext => {
147
152
  return (
@@ -158,7 +163,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
158
163
  if (!id) {
159
164
  throw new Error('Column id is required');
160
165
  }
161
- finalColumns.push(columnHelper.accessor(id as any, { ...column, id }));
166
+ finalColumns.push(columnHelper.accessor(id as any, { ...column, id, enableColumnFilter: false }));
162
167
  }
163
168
 
164
169
  if (defaultColumnOrder) {
@@ -259,6 +264,29 @@ function getRowActions(
259
264
  };
260
265
  }
261
266
 
267
+ function DefaultDisplayComponent({ value, fieldInfo }: { value: any; fieldInfo: FieldInfo }) {
268
+ if (fieldInfo.list && Array.isArray(value)) {
269
+ return value.join(', ');
270
+ }
271
+ if ((fieldInfo.type === 'DateTime' && typeof value === 'string') || value instanceof Date) {
272
+ return <DisplayComponent id="vendure:dateTime" value={value} />;
273
+ }
274
+ if (fieldInfo.type === 'Boolean') {
275
+ if (fieldInfo.name === 'enabled') {
276
+ return <DisplayComponent id="vendure:booleanBadge" value={value} />;
277
+ } else {
278
+ return <DisplayComponent id="vendure:booleanCheckbox" value={value} />;
279
+ }
280
+ }
281
+ if (fieldInfo.type === 'Asset') {
282
+ return <DisplayComponent id="vendure:asset" value={value} />;
283
+ }
284
+ if (value !== null && typeof value === 'object') {
285
+ return <DisplayComponent id="vendure:json" value={value} />;
286
+ }
287
+ return value;
288
+ }
289
+
262
290
  function DeleteMutationRowAction({
263
291
  deleteMutation,
264
292
  row,
@@ -7,9 +7,8 @@ import {
7
7
  } from '@/vdb/components/ui/breadcrumb.js';
8
8
  import type { NavMenuItem, NavMenuSection } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
9
9
  import { getNavMenuConfig } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
10
- import { useDisplayLocale } from '@/vdb/hooks/use-display-locale.js';
11
10
  import { useLingui } from '@lingui/react';
12
- import { Link, useRouter, useRouterState } from '@tanstack/react-router';
11
+ import { Link, useRouterState } from '@tanstack/react-router';
13
12
  import * as React from 'react';
14
13
  import { Fragment } from 'react';
15
14
 
@@ -25,11 +24,8 @@ export type PageBreadcrumb = BreadcrumbPair | BreadcrumbShorthand;
25
24
  export function GeneratedBreadcrumbs() {
26
25
  const matches = useRouterState({ select: s => s.matches });
27
26
  const currentPath = useRouterState({ select: s => s.location.pathname });
28
- const router = useRouter();
29
27
  const { i18n } = useLingui();
30
28
  const navMenuConfig = getNavMenuConfig();
31
- const { bcp47Tag } = useDisplayLocale();
32
- const basePath = router.basepath || '';
33
29
 
34
30
  const normalizeBreadcrumb = (breadcrumb: any, pathname: string): BreadcrumbPair[] => {
35
31
  if (typeof breadcrumb === 'string') {
@@ -58,12 +54,11 @@ export function GeneratedBreadcrumbs() {
58
54
  .flatMap(({ pathname, loaderData }) => normalizeBreadcrumb(loaderData.breadcrumb, pathname));
59
55
  }, [matches]);
60
56
 
61
- const isBaseRoute = (p: string) => p === basePath || p === `${basePath}/`;
57
+ const isBaseRoute = (p: string) => p === '' || p === `/`;
62
58
  const pageCrumbs: BreadcrumbPair[] = rawCrumbs.filter(c => !isBaseRoute(c.path));
63
59
 
64
60
  const normalizePath = (path: string): string => {
65
- const normalizedPath = basePath && path.startsWith(basePath) ? path.slice(basePath.length) : path;
66
- return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
61
+ return path.startsWith('/') ? path : `/${path}`;
67
62
  };
68
63
 
69
64
  const pathMatches = (cleanPath: string, rawUrl?: string): boolean => {
@@ -115,10 +110,7 @@ export function GeneratedBreadcrumbs() {
115
110
  return undefined;
116
111
  };
117
112
 
118
- const sectionCrumb = React.useMemo(
119
- () => findSectionCrumb(currentPath),
120
- [currentPath, basePath, navMenuConfig],
121
- );
113
+ const sectionCrumb = React.useMemo(() => findSectionCrumb(currentPath), [currentPath, navMenuConfig]);
122
114
  const breadcrumbs: BreadcrumbPair[] = React.useMemo(() => {
123
115
  const arr = sectionCrumb ? [sectionCrumb, ...pageCrumbs] : pageCrumbs;
124
116
  return arr.filter(