@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
@@ -1,4 +1,5 @@
1
1
  import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
2
+ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
2
3
  import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
3
4
  import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
4
5
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -24,15 +25,21 @@ import {
24
25
  } from '@/vdb/framework/layout-engine/page-layout.js';
25
26
  import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
26
27
  import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
28
+ import { api } from '@/vdb/graphql/api.js';
27
29
  import { useChannel } from '@/vdb/hooks/use-channel.js';
28
30
  import { Trans, useLingui } from '@lingui/react/macro';
31
+ import { useQuery } from '@tanstack/react-query';
29
32
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
30
- import { Fragment } from 'react/jsx-runtime';
33
+ import { VariablesOf } from 'gql.tada';
34
+ import { Trash } from 'lucide-react';
31
35
  import { toast } from 'sonner';
36
+ import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
37
+ import { AddStockLocationDropdown } from './components/add-stock-location-dropdown.js';
32
38
  import { VariantPriceDetail } from './components/variant-price-detail.js';
33
39
  import {
34
40
  createProductVariantDocument,
35
41
  productVariantDetailDocument,
42
+ stockLocationsQueryDocument,
36
43
  updateProductVariantDocument,
37
44
  } from './product-variants.graphql.js';
38
45
 
@@ -57,6 +64,8 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
57
64
  errorComponent: ({ error }) => <ErrorPage message={error.message} />,
58
65
  });
59
66
 
67
+ type PriceInput = NonNullable<VariablesOf<typeof updateProductVariantDocument>['input']['prices']>[number];
68
+
60
69
  function ProductVariantDetailPage() {
61
70
  const params = Route.useParams();
62
71
  const navigate = useNavigate();
@@ -64,6 +73,11 @@ function ProductVariantDetailPage() {
64
73
  const { t } = useLingui();
65
74
  const { activeChannel } = useChannel();
66
75
 
76
+ const { data: stockLocationsData } = useQuery({
77
+ queryKey: ['stockLocations'],
78
+ queryFn: () => api.query(stockLocationsQueryDocument, {}),
79
+ });
80
+
67
81
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
68
82
  pageId,
69
83
  queryDocument: productVariantDetailDocument,
@@ -79,7 +93,7 @@ function ProductVariantDetailPage() {
79
93
  facetValueIds: entity.facetValues.map(facetValue => facetValue.id),
80
94
  taxCategoryId: entity.taxCategory.id,
81
95
  price: entity.price,
82
- prices: [],
96
+ prices: entity.prices,
83
97
  trackInventory: entity.trackInventory,
84
98
  outOfStockThreshold: entity.outOfStockThreshold,
85
99
  stockLevels: entity.stockLevels.map(stockLevel => ({
@@ -117,7 +131,74 @@ function ProductVariantDetailPage() {
117
131
  },
118
132
  });
119
133
 
120
- const [price, taxCategoryId] = form.watch(['price', 'taxCategoryId']);
134
+ const availableCurrencies = activeChannel?.availableCurrencyCodes ?? [];
135
+ const [prices, taxCategoryId, stockLevels] = form.watch(['prices', 'taxCategoryId', 'stockLevels']);
136
+
137
+ // Filter out deleted prices for display
138
+ const activePrices = prices?.filter(p => !p.delete) ?? [];
139
+
140
+ // Get currencies that are currently active (not deleted)
141
+ const usedCurrencies = activePrices.map(p => p.currencyCode);
142
+ const unusedCurrencies = availableCurrencies.filter(c => !usedCurrencies.includes(c));
143
+
144
+ // Get used stock location IDs
145
+ const usedStockLocationIds = stockLevels?.map(sl => sl.stockLocationId) ?? [];
146
+
147
+ const handleAddCurrency = (currencyCode: string) => {
148
+ const currentPrices = form.getValues('prices') || [];
149
+
150
+ // Check if this currency already exists (including deleted ones)
151
+ const existingPriceIndex = currentPrices.findIndex(p => p.currencyCode === currencyCode);
152
+
153
+ if (existingPriceIndex !== -1) {
154
+ // Currency exists, mark it as not deleted
155
+ const updatedPrices = [...currentPrices];
156
+ updatedPrices[existingPriceIndex] = {
157
+ ...updatedPrices[existingPriceIndex],
158
+ delete: false,
159
+ };
160
+ form.setValue('prices', updatedPrices, {
161
+ shouldDirty: true,
162
+ shouldValidate: true,
163
+ });
164
+ } else {
165
+ // Add new currency
166
+ const newPrice = {
167
+ currencyCode,
168
+ price: 0,
169
+ delete: false,
170
+ } as PriceInput;
171
+ form.setValue('prices', [...currentPrices, newPrice], {
172
+ shouldDirty: true,
173
+ shouldValidate: true,
174
+ });
175
+ }
176
+ };
177
+
178
+ const handleRemoveCurrency = (indexToRemove: number) => {
179
+ const currentPrices = form.getValues('prices') || [];
180
+ const updatedPrices = [...currentPrices];
181
+ updatedPrices[indexToRemove] = {
182
+ ...updatedPrices[indexToRemove],
183
+ delete: true,
184
+ };
185
+ form.setValue('prices', updatedPrices, {
186
+ shouldDirty: true,
187
+ shouldValidate: true,
188
+ });
189
+ };
190
+
191
+ const handleAddStockLocation = (stockLocationId: string, stockLocationName: string) => {
192
+ const currentStockLevels = form.getValues('stockLevels') || [];
193
+ const newStockLevel = {
194
+ stockLocationId,
195
+ stockOnHand: 0,
196
+ };
197
+ form.setValue('stockLevels', [...currentStockLevels, newStockLevel], {
198
+ shouldDirty: true,
199
+ shouldValidate: true,
200
+ });
201
+ };
121
202
 
122
203
  return (
123
204
  <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
@@ -168,7 +249,7 @@ function ProductVariantDetailPage() {
168
249
  <CustomFieldsPageBlock column="main" entityType="ProductVariant" control={form.control} />
169
250
 
170
251
  <PageBlock column="main" blockId="price-and-tax" title={<Trans>Price and tax</Trans>}>
171
- <div className="grid grid-cols-2 gap-4 items-start">
252
+ <DetailFormGrid>
172
253
  <FormFieldWrapper
173
254
  control={form.control}
174
255
  name="taxCategoryId"
@@ -177,56 +258,62 @@ function ProductVariantDetailPage() {
177
258
  <TaxCategorySelector value={field.value} onChange={field.onChange} />
178
259
  )}
179
260
  />
261
+ </DetailFormGrid>
262
+ {activePrices.map((price, displayIndex) => {
263
+ // Find the actual index in the full prices array
264
+ const actualIndex = prices?.findIndex(p => p === price) ?? displayIndex;
180
265
 
181
- <div>
182
- <FormFieldWrapper
183
- control={form.control}
184
- name="price"
185
- label={<Trans>Price</Trans>}
186
- render={({ field }) => (
187
- <MoneyInput {...field} currency={entity?.currencyCode} />
188
- )}
189
- />
190
- <VariantPriceDetail
191
- priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
192
- price={price}
193
- currencyCode={
194
- entity?.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
195
- }
196
- taxCategoryId={taxCategoryId}
197
- />
198
- </div>
199
- </div>
266
+ const currencyCodeLabel = (
267
+ <div className="uppercase text-muted-foreground">{price.currencyCode}</div>
268
+ );
269
+ const priceLabel = (
270
+ <div className="flex gap-1 items-center justify-between">
271
+ <Trans>Price</Trans> {activePrices.length > 1 ? currencyCodeLabel : null}
272
+ </div>
273
+ );
274
+ return (
275
+ <DetailFormGrid key={price.currencyCode}>
276
+ <div className="flex gap-1 items-end">
277
+ <FormFieldWrapper
278
+ control={form.control}
279
+ name={`prices.${actualIndex}.price`}
280
+ label={priceLabel}
281
+ render={({ field }) => (
282
+ <MoneyInput {...field} currency={price.currencyCode} />
283
+ )}
284
+ />
285
+ {activePrices.length > 1 && (
286
+ <Button
287
+ type="button"
288
+ variant="ghost"
289
+ size="sm"
290
+ onClick={() => handleRemoveCurrency(actualIndex)}
291
+ className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
292
+ >
293
+ <Trash className="size-4" />
294
+ </Button>
295
+ )}
296
+ </div>
297
+ <VariantPriceDetail
298
+ priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
299
+ price={price.price}
300
+ currencyCode={
301
+ price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
302
+ }
303
+ taxCategoryId={taxCategoryId}
304
+ />
305
+ </DetailFormGrid>
306
+ );
307
+ })}
308
+ {unusedCurrencies.length ? (
309
+ <AddCurrencyDropdown
310
+ onCurrencySelect={handleAddCurrency}
311
+ unusedCurrencies={unusedCurrencies}
312
+ />
313
+ ) : null}
200
314
  </PageBlock>
201
315
  <PageBlock column="main" blockId="stock" title={<Trans>Stock</Trans>}>
202
316
  <DetailFormGrid>
203
- {entity?.stockLevels.map((stockLevel, index) => (
204
- <Fragment key={stockLevel.id}>
205
- <FormFieldWrapper
206
- control={form.control}
207
- name={`stockLevels.${index}.stockOnHand`}
208
- label={<Trans>Stock level</Trans>}
209
- render={({ field }) => (
210
- <Input
211
- type="number"
212
- value={field.value}
213
- onChange={e => {
214
- field.onChange(e.target.valueAsNumber);
215
- }}
216
- />
217
- )}
218
- />
219
- <div>
220
- <FormItem>
221
- <FormLabel>
222
- <Trans>Allocated</Trans>
223
- </FormLabel>
224
- <div className="text-sm pt-1.5">{stockLevel.stockAllocated}</div>
225
- </FormItem>
226
- </div>
227
- </Fragment>
228
- ))}
229
-
230
317
  <FormFieldWrapper
231
318
  control={form.control}
232
319
  name="trackInventory"
@@ -292,6 +379,47 @@ function ProductVariantDetailPage() {
292
379
  )}
293
380
  />
294
381
  </DetailFormGrid>
382
+ {stockLevels?.map((stockLevel, index) => {
383
+ const stockAllocated =
384
+ entity?.stockLevels.find(sl => sl.stockLocation.id === stockLevel.stockLocationId)
385
+ ?.stockAllocated ?? 0;
386
+ const stockLocationName = stockLocationsData?.stockLocations.items?.find(
387
+ sl => sl.id === stockLevel.stockLocationId,
388
+ )?.name;
389
+ const stockLocationNameLabel =
390
+ stockLevels.length > 1 ? (
391
+ <div className="text-muted-foreground">{stockLocationName}</div>
392
+ ) : null;
393
+ const stockLabel = (
394
+ <>
395
+ <Trans>Stock level</Trans>
396
+ {stockLocationNameLabel}
397
+ </>
398
+ );
399
+ return (
400
+ <DetailFormGrid key={stockLevel.stockLocationId}>
401
+ <FormFieldWrapper
402
+ control={form.control}
403
+ name={`stockLevels.${index}.stockOnHand`}
404
+ label={stockLabel}
405
+ render={({ field }) => <NumberInput {...field} value={field.value} />}
406
+ />
407
+ <div>
408
+ <FormItem>
409
+ <FormLabel>
410
+ <Trans>Allocated</Trans>
411
+ </FormLabel>
412
+ <div className="text-sm pt-1.5">{stockAllocated}</div>
413
+ </FormItem>
414
+ </div>
415
+ </DetailFormGrid>
416
+ );
417
+ })}
418
+ <AddStockLocationDropdown
419
+ availableStockLocations={stockLocationsData?.stockLocations.items ?? []}
420
+ usedStockLocationIds={usedStockLocationIds}
421
+ onStockLocationSelect={handleAddStockLocation}
422
+ />
295
423
  </PageBlock>
296
424
 
297
425
  <PageBlock column="side" blockId="facet-values">
@@ -108,12 +108,6 @@ export const DuplicateProductsBulkAction: BulkActionComponent<any> = ({ selectio
108
108
  <DuplicateBulkAction
109
109
  entityType="Product"
110
110
  duplicatorCode="product-duplicator"
111
- duplicatorArguments={[
112
- {
113
- name: 'includeVariants',
114
- value: 'true',
115
- },
116
- ]}
117
111
  requiredPermissions={['UpdateCatalog', 'UpdateProduct']}
118
112
  entityName="Product"
119
113
  selection={selection}
@@ -5,7 +5,6 @@ import {
5
5
  PaginatedListRefresherRegisterFn,
6
6
  } from '@/vdb/components/shared/paginated-list-data-table.js';
7
7
  import { StockLevelLabel } from '@/vdb/components/shared/stock-level-label.js';
8
- import { graphql } from '@/vdb/graphql/graphql.js';
9
8
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
10
9
  import { ColumnFiltersState, SortingState } from '@tanstack/react-table';
11
10
  import { useState } from 'react';
@@ -17,15 +16,6 @@ import {
17
16
  } from '../../_product-variants/components/product-variant-bulk-actions.js';
18
17
  import { productVariantListDocument } from '../products.graphql.js';
19
18
 
20
- export const deleteProductVariantDocument = graphql(`
21
- mutation DeleteProductVariant($id: ID!) {
22
- deleteProductVariant(id: $id) {
23
- result
24
- message
25
- }
26
- }
27
- `);
28
-
29
19
  interface ProductVariantsTableProps {
30
20
  productId: string;
31
21
  registerRefresher?: PaginatedListRefresherRegisterFn;
@@ -47,7 +37,6 @@ export function ProductVariantsTable({
47
37
  <PaginatedListDataTable
48
38
  registerRefresher={registerRefresher}
49
39
  listQuery={productVariantListDocument}
50
- deleteMutation={deleteProductVariantDocument}
51
40
  transformVariables={variables => ({
52
41
  ...variables,
53
42
  productId,
@@ -1,5 +1,6 @@
1
1
  import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
2
2
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
3
+ import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
3
4
  import { Button } from '@/vdb/components/ui/button.js';
4
5
  import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
5
6
  import { ListPage } from '@/vdb/framework/page/list-page.js';
@@ -7,7 +8,7 @@ import { api } from '@/vdb/graphql/api.js';
7
8
  import { Trans, useLingui } from '@lingui/react/macro';
8
9
  import { useMutation } from '@tanstack/react-query';
9
10
  import { createFileRoute, Link } from '@tanstack/react-router';
10
- import { PlusIcon, RefreshCwIcon } from 'lucide-react';
11
+ import { ListRestart, PlusIcon } from 'lucide-react';
11
12
  import { toast } from 'sonner';
12
13
  import {
13
14
  AssignFacetValuesToProductsBulkAction,
@@ -48,6 +49,9 @@ function ProductListPage() {
48
49
  name: {
49
50
  cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
50
51
  },
52
+ description: {
53
+ cell: RichTextDescriptionCell,
54
+ },
51
55
  }}
52
56
  onSearchTermChange={searchTerm => {
53
57
  return {
@@ -87,7 +91,7 @@ function ProductListPage() {
87
91
  <PageActionBarRight>
88
92
  <PermissionGuard requires={['UpdateCatalog']}>
89
93
  <Button variant="outline" onClick={handleRebuildSearchIndex}>
90
- <RefreshCwIcon />
94
+ <ListRestart />
91
95
  <Trans>Rebuild search index</Trans>
92
96
  </Button>
93
97
  </PermissionGuard>
@@ -124,11 +124,9 @@ function ProductOptionDetailPage() {
124
124
  params: { id: params.id },
125
125
  onSuccess: async data => {
126
126
  toast(
127
- i18n.t(
128
- creatingNewEntity
129
- ? 'Successfully created product option'
130
- : 'Successfully updated product option',
131
- ),
127
+ creatingNewEntity
128
+ ? t`Successfully created product option`
129
+ : t`Successfully updated product option`,
132
130
  );
133
131
  resetForm();
134
132
  const created = Array.isArray(data) ? data[0] : data;
@@ -138,9 +136,7 @@ function ProductOptionDetailPage() {
138
136
  },
139
137
  onError: err => {
140
138
  toast(
141
- i18n.t(
142
- creatingNewEntity ? 'Failed to create product option' : 'Failed to update product option',
143
- ),
139
+ creatingNewEntity ? t`Failed to create product option` : t`Failed to update product option`,
144
140
  {
145
141
  description: err instanceof Error ? err.message : 'Unknown error',
146
142
  },
@@ -63,16 +63,6 @@ export const DuplicatePromotionsBulkAction: BulkActionComponent<any> = ({ select
63
63
  <DuplicateBulkAction
64
64
  entityType="Promotion"
65
65
  duplicatorCode="promotion-duplicator"
66
- duplicatorArguments={[
67
- {
68
- name: 'includeConditions',
69
- value: 'true',
70
- },
71
- {
72
- name: 'includeActions',
73
- value: 'true',
74
- },
75
- ]}
76
66
  requiredPermissions={['CreatePromotion']}
77
67
  entityName="Promotion"
78
68
  selection={selection}
@@ -2,8 +2,8 @@ import { configurableOperationFragment } from '@/vdb/graphql/fragments.js';
2
2
  import { graphql } from '@/vdb/graphql/graphql.js';
3
3
 
4
4
  export const promotionListDocument = graphql(`
5
- query PromotionList {
6
- promotions {
5
+ query PromotionList($options: PromotionListOptions) {
6
+ promotions(options: $options) {
7
7
  items {
8
8
  id
9
9
  createdAt
@@ -1,6 +1,7 @@
1
1
  import { BooleanDisplayBadge } from '@/vdb/components/data-display/boolean.js';
2
2
  import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
3
3
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
4
+ import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
4
5
  import { Button } from '@/vdb/components/ui/button.js';
5
6
  import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
6
7
  import { ListPage } from '@/vdb/framework/page/list-page.js';
@@ -40,6 +41,14 @@ function PromotionListPage() {
40
41
  couponCode: { contains: searchTerm },
41
42
  };
42
43
  }}
44
+ transformVariables={variables => {
45
+ return {
46
+ options: {
47
+ ...variables.options,
48
+ filterOperator: 'OR' as const,
49
+ },
50
+ };
51
+ }}
43
52
  customizeColumns={{
44
53
  name: {
45
54
  cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
@@ -47,6 +56,9 @@ function PromotionListPage() {
47
56
  enabled: {
48
57
  cell: ({ row }) => <BooleanDisplayBadge value={row.original.enabled} />,
49
58
  },
59
+ description: {
60
+ cell: RichTextDescriptionCell,
61
+ },
50
62
  }}
51
63
  bulkActions={[
52
64
  {
@@ -1,4 +1,5 @@
1
1
  import { DateTimeInput } from '@/vdb/components/data-input/datetime-input.js';
2
+ import { NumberInput } from '@/vdb/components/data-input/number-input.js';
2
3
  import { RichTextInput } from '@/vdb/components/data-input/rich-text-input.js';
3
4
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
4
5
  import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
@@ -203,11 +204,7 @@ function PromotionDetailPage() {
203
204
  name="perCustomerUsageLimit"
204
205
  label={<Trans>Per customer usage limit</Trans>}
205
206
  render={({ field }) => (
206
- <Input
207
- type="number"
208
- value={field.value ?? ''}
209
- onChange={e => field.onChange(e.target.valueAsNumber)}
210
- />
207
+ <NumberInput {...field} value={field.value ?? ''} min={0} max={1000} />
211
208
  )}
212
209
  />
213
210
  <FormFieldWrapper
@@ -215,11 +212,7 @@ function PromotionDetailPage() {
215
212
  name="usageLimit"
216
213
  label={<Trans>Usage limit</Trans>}
217
214
  render={({ field }) => (
218
- <Input
219
- type="number"
220
- value={field.value ?? ''}
221
- onChange={e => field.onChange(e.target.valueAsNumber)}
222
- />
215
+ <NumberInput {...field} value={field.value ?? ''} min={0} max={1000} />
223
216
  )}
224
217
  />
225
218
  </DetailFormGrid>
@@ -11,8 +11,8 @@ export const sellerItemFragment = graphql(`
11
11
 
12
12
  export const sellerListQuery = graphql(
13
13
  `
14
- query SellerList {
15
- sellers {
14
+ query SellerList($options: SellerListOptions) {
15
+ sellers(options: $options) {
16
16
  items {
17
17
  ...SellerItem
18
18
  }
@@ -15,8 +15,8 @@ export const shippingMethodItemFragment = graphql(`
15
15
 
16
16
  export const shippingMethodListQuery = graphql(
17
17
  `
18
- query ShippingMethodList {
19
- shippingMethods {
18
+ query ShippingMethodList($options: ShippingMethodListOptions) {
19
+ shippingMethods(options: $options) {
20
20
  items {
21
21
  ...ShippingMethodItem
22
22
  }
@@ -1,5 +1,6 @@
1
1
  import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
2
2
  import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
3
+ import { RichTextDescriptionCell } from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
3
4
  import { Button } from '@/vdb/components/ui/button.js';
4
5
  import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
5
6
  import { ListPage } from '@/vdb/framework/page/list-page.js';
@@ -35,6 +36,9 @@ function ShippingMethodListPage() {
35
36
  name: {
36
37
  cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
37
38
  },
39
+ description: {
40
+ cell: RichTextDescriptionCell,
41
+ },
38
42
  }}
39
43
  onSearchTermChange={searchTerm => {
40
44
  return {
@@ -86,11 +86,9 @@ function ShippingMethodDetailPage() {
86
86
  params: { id: params.id },
87
87
  onSuccess: async data => {
88
88
  toast.success(
89
- i18n.t(
90
- creatingNewEntity
91
- ? 'Successfully created shipping method'
92
- : 'Successfully updated shipping method',
93
- ),
89
+ creatingNewEntity
90
+ ? t`Successfully created shipping method`
91
+ : t`Successfully updated shipping method`,
94
92
  );
95
93
  resetForm();
96
94
  if (creatingNewEntity) {
@@ -99,11 +97,7 @@ function ShippingMethodDetailPage() {
99
97
  },
100
98
  onError: err => {
101
99
  toast.error(
102
- i18n.t(
103
- creatingNewEntity
104
- ? 'Failed to create shipping method'
105
- : 'Failed to update shipping method',
106
- ),
100
+ creatingNewEntity ? t`Failed to create shipping method` : t`Failed to update shipping method`,
107
101
  {
108
102
  description: err instanceof Error ? err.message : 'Unknown error',
109
103
  },
@@ -12,8 +12,8 @@ export const stockLocationFragment = graphql(`
12
12
 
13
13
  export const stockLocationListQuery = graphql(
14
14
  `
15
- query StockLocationList {
16
- stockLocations {
15
+ query StockLocationList($options: StockLocationListOptions) {
16
+ stockLocations(options: $options) {
17
17
  items {
18
18
  ...StockLocationItem
19
19
  }
@@ -12,8 +12,8 @@ export const taxCategoryItemFragment = graphql(`
12
12
 
13
13
  export const taxCategoryListQuery = graphql(
14
14
  `
15
- query TaxCategoryList {
16
- taxCategories {
15
+ query TaxCategoryList($options: TaxCategoryListOptions) {
16
+ taxCategories(options: $options) {
17
17
  items {
18
18
  ...TaxCategoryItem
19
19
  }
@@ -8,6 +8,7 @@ import { api } from '@/vdb/graphql/api.js';
8
8
  import { Trans, useLingui } from '@lingui/react/macro';
9
9
  import { createFileRoute, Link } from '@tanstack/react-router';
10
10
  import { PlusIcon } from 'lucide-react';
11
+ import { mapFacetedFilterFields } from '../../../common/map-faceted-filter-fields.js';
11
12
  import { taxCategoryListQuery } from '../_tax-categories/tax-categories.graphql.js';
12
13
  import { zoneListQuery } from '../_zones/zones.graphql.js';
13
14
  import { DeleteTaxRatesBulkAction } from './components/tax-rate-bulk-actions.js';
@@ -42,6 +43,14 @@ function TaxRateListPage() {
42
43
  name: { contains: searchTerm },
43
44
  };
44
45
  }}
46
+ transformVariables={input => {
47
+ const facetedFilters = input.options?.filter?._and ?? [];
48
+ mapFacetedFilterFields(facetedFilters, {
49
+ category: 'categoryId',
50
+ zone: 'zoneId',
51
+ });
52
+ return input;
53
+ }}
45
54
  facetedFilters={{
46
55
  enabled: {
47
56
  title: t`Enabled`,
@@ -125,6 +125,7 @@ function TaxRateDetailPage() {
125
125
  {...field}
126
126
  type="number"
127
127
  suffix="%"
128
+ min={0}
128
129
  value={field.value}
129
130
  onChange={e => field.onChange(e.target.valueAsNumber)}
130
131
  />
@@ -11,8 +11,8 @@ export const zoneItemFragment = graphql(`
11
11
 
12
12
  export const zoneListQuery = graphql(
13
13
  `
14
- query ZoneList {
15
- zones {
14
+ query ZoneList($options: ZoneListOptions) {
15
+ zones(options: $options) {
16
16
  items {
17
17
  ...ZoneItem
18
18
  }
@@ -36,8 +36,8 @@ function LoginPage() {
36
36
  const isVerifying = isLoading || auth.status === 'verifying';
37
37
 
38
38
  return (
39
- <div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10">
40
- <div className="w-full max-w-sm md:max-w-4xl">
39
+ <div className="flex min-h-svh flex-col items-center justify-center p-6 md:p-10 bg-sidebar">
40
+ <div className="w-full max-w-sm md:max-w-md">
41
41
  <LoginForm
42
42
  onFormSubmit={onFormSubmit}
43
43
  isVerifying={isVerifying}