@vendure/dashboard 3.5.3-master-202601300300 → 3.5.3

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 (25) hide show
  1. package/dist/vite/utils/plugin-discovery.js +3 -3
  2. package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
  3. package/dist/vite/vite-plugin-lingui-babel.js +90 -8
  4. package/dist/vite/vite-plugin-translations.js +2 -2
  5. package/package.json +3 -3
  6. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +22 -3
  7. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
  8. package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
  9. package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
  10. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
  11. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -0
  12. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
  13. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
  14. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -0
  15. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -0
  16. package/src/i18n/locales/ar.po +58 -5
  17. package/src/i18n/locales/en.po +58 -5
  18. package/src/lib/components/data-input/index.ts +1 -0
  19. package/src/lib/components/ui/alert.tsx +2 -0
  20. package/src/lib/framework/extension-api/input-component-extensions.tsx +2 -0
  21. package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
  22. package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
  23. package/src/lib/framework/page/detail-page.tsx +22 -37
  24. package/src/lib/graphql/graphql-env.d.ts +30 -13
  25. package/src/lib/hooks/use-job-queue-polling.ts +160 -0
@@ -323,9 +323,9 @@ export async function findVendurePluginFiles({ outputPath, vendureConfigPath, lo
323
323
  const globStart = Date.now();
324
324
  const files = await glob(patterns, {
325
325
  ignore: [
326
- // Skip nested node_modules (transitive deps) but not .pnpm directory.
327
- // [!.] ensures .pnpm paths are kept since pnpm stores all packages there.
328
- '**/node_modules/[!.pnpm]*/**/node_modules/**',
326
+ // Skip nested node_modules (transitive deps) but not .pnpm or .bun directories.
327
+ // [!.] excludes paths starting with . since pnpm and bun store packages there.
328
+ '**/node_modules/[!.]*/**/node_modules/**',
329
329
  '**/*.spec.js',
330
330
  '**/*.test.js',
331
331
  ],
@@ -1,4 +1,14 @@
1
1
  import type { Plugin } from 'vite';
2
+ /**
3
+ * Options for the linguiBabelPlugin.
4
+ */
5
+ export interface LinguiBabelPluginOptions {
6
+ /**
7
+ * For testing: manually specify package paths that should have Lingui macros transformed.
8
+ * In production, these are automatically discovered from the VendureConfig plugins.
9
+ */
10
+ additionalPackagePaths?: string[];
11
+ }
2
12
  /**
3
13
  * @description
4
14
  * A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
@@ -17,11 +27,14 @@ import type { Plugin } from 'vite';
17
27
  * - `@vendure/dashboard/src` files (in node_modules for external projects)
18
28
  * - `packages/dashboard/src` files (in monorepo development)
19
29
  * - User's dashboard extension files (e.g., custom plugins using Lingui)
30
+ * - Third-party npm packages that provide dashboard extensions (discovered automatically)
20
31
  *
21
32
  * Files NOT processed:
22
- * - Other node_modules packages (they shouldn't contain Lingui macros)
33
+ * - Files that don't contain Lingui macro imports (fast check via string matching)
34
+ * - Non-JS/TS files
35
+ * - node_modules packages that are not discovered as Vendure plugins
23
36
  *
24
37
  * @see https://github.com/vendurehq/vendure/issues/3929
25
38
  * @see https://github.com/lingui/swc-plugin/issues/179
26
39
  */
27
- export declare function linguiBabelPlugin(): Plugin;
40
+ export declare function linguiBabelPlugin(options?: LinguiBabelPluginOptions): Plugin;
@@ -1,4 +1,5 @@
1
1
  import * as babel from '@babel/core';
2
+ import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
2
3
  /**
3
4
  * @description
4
5
  * A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
@@ -17,19 +18,40 @@ import * as babel from '@babel/core';
17
18
  * - `@vendure/dashboard/src` files (in node_modules for external projects)
18
19
  * - `packages/dashboard/src` files (in monorepo development)
19
20
  * - User's dashboard extension files (e.g., custom plugins using Lingui)
21
+ * - Third-party npm packages that provide dashboard extensions (discovered automatically)
20
22
  *
21
23
  * Files NOT processed:
22
- * - Other node_modules packages (they shouldn't contain Lingui macros)
24
+ * - Files that don't contain Lingui macro imports (fast check via string matching)
25
+ * - Non-JS/TS files
26
+ * - node_modules packages that are not discovered as Vendure plugins
23
27
  *
24
28
  * @see https://github.com/vendurehq/vendure/issues/3929
25
29
  * @see https://github.com/lingui/swc-plugin/issues/179
26
30
  */
27
- export function linguiBabelPlugin() {
31
+ export function linguiBabelPlugin(options) {
32
+ var _a;
33
+ // Paths of npm packages that should have Lingui macros transformed.
34
+ // This is populated from plugin discovery when transform is first called.
35
+ const allowedNodeModulesPackages = new Set((_a = options === null || options === void 0 ? void 0 : options.additionalPackagePaths) !== null && _a !== void 0 ? _a : []);
36
+ // API reference to the config loader plugin (set in configResolved)
37
+ let configLoaderApi;
38
+ // Cached result from config loader (set on first transform that needs it)
39
+ let configResult;
28
40
  return {
29
41
  name: 'vendure:lingui-babel',
30
42
  // Run BEFORE @vitejs/plugin-react so the macros are already transformed
31
43
  // when the react plugin processes the file
32
44
  enforce: 'pre',
45
+ configResolved({ plugins }) {
46
+ // Get reference to the config loader API.
47
+ // This doesn't load the config yet - that happens lazily in transform.
48
+ try {
49
+ configLoaderApi = getConfigLoaderApi(plugins);
50
+ }
51
+ catch (_a) {
52
+ // configLoaderPlugin not available (e.g., plugin used standalone for testing)
53
+ }
54
+ },
33
55
  async transform(code, id) {
34
56
  // Strip query params for path matching (Vite adds ?v=xxx for cache busting)
35
57
  const cleanId = id.split('?')[0];
@@ -42,15 +64,42 @@ export function linguiBabelPlugin() {
42
64
  if (!code.includes('@lingui/') || !code.includes('/macro')) {
43
65
  return null;
44
66
  }
45
- // Skip node_modules files EXCEPT for @vendure/dashboard source
46
- // This ensures:
47
- // 1. Dashboard source files get transformed (both in monorepo and external projects)
48
- // 2. User's extension files get transformed (not in node_modules)
49
- // 3. Other node_modules packages are left alone
67
+ // Check if this file should be transformed
50
68
  if (cleanId.includes('node_modules')) {
69
+ // Always allow @vendure/dashboard source files
51
70
  const isVendureDashboard = cleanId.includes('@vendure/dashboard/src') || cleanId.includes('packages/dashboard/src');
52
71
  if (!isVendureDashboard) {
53
- return null;
72
+ // Load discovered plugins on first need (lazy loading with caching)
73
+ if (configLoaderApi && !configResult) {
74
+ try {
75
+ configResult = await configLoaderApi.getVendureConfig();
76
+ // Extract package paths from discovered npm plugins
77
+ for (const plugin of configResult.pluginInfo) {
78
+ if (!plugin.sourcePluginPath && plugin.pluginPath.includes('node_modules')) {
79
+ const packagePath = extractPackagePath(plugin.pluginPath);
80
+ if (packagePath) {
81
+ allowedNodeModulesPackages.add(packagePath);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ catch (error) {
87
+ // Log but continue - will use only manually specified paths
88
+ // eslint-disable-next-line no-console
89
+ console.warn('[vendure:lingui-babel] Failed to load plugin config:', error);
90
+ }
91
+ }
92
+ // Check if this is from a discovered Vendure plugin package
93
+ let isDiscoveredPlugin = false;
94
+ for (const pkgPath of allowedNodeModulesPackages) {
95
+ if (cleanId.includes(pkgPath)) {
96
+ isDiscoveredPlugin = true;
97
+ break;
98
+ }
99
+ }
100
+ if (!isDiscoveredPlugin) {
101
+ return null;
102
+ }
54
103
  }
55
104
  }
56
105
  try {
@@ -84,3 +133,36 @@ export function linguiBabelPlugin() {
84
133
  },
85
134
  };
86
135
  }
136
+ /**
137
+ * Extracts the npm package name from a full file path.
138
+ *
139
+ * Examples:
140
+ * - /path/to/node_modules/@vendure-ee/plugin/dist/index.js -> @vendure-ee/plugin
141
+ * - /path/to/node_modules/some-plugin/lib/index.js -> some-plugin
142
+ * - /path/to/node_modules/.pnpm/@vendure-ee+plugin@1.0.0/node_modules/@vendure-ee/plugin/dist/index.js -> @vendure-ee/plugin
143
+ */
144
+ function extractPackagePath(filePath) {
145
+ // Normalize path separators
146
+ const normalizedPath = filePath.replace(/\\/g, '/');
147
+ // Find the last occurrence of node_modules (handles pnpm structure)
148
+ const lastNodeModulesIndex = normalizedPath.lastIndexOf('node_modules/');
149
+ if (lastNodeModulesIndex === -1) {
150
+ return undefined;
151
+ }
152
+ const afterNodeModules = normalizedPath.slice(lastNodeModulesIndex + 'node_modules/'.length);
153
+ // Handle scoped packages (@scope/package)
154
+ if (afterNodeModules.startsWith('@')) {
155
+ const parts = afterNodeModules.split('/');
156
+ if (parts.length >= 2) {
157
+ return `${parts[0]}/${parts[1]}`;
158
+ }
159
+ }
160
+ else {
161
+ // Unscoped package
162
+ const parts = afterNodeModules.split('/');
163
+ if (parts.length >= 1) {
164
+ return parts[0];
165
+ }
166
+ }
167
+ return undefined;
168
+ }
@@ -91,8 +91,8 @@ async function getPluginTranslations(pluginInfo) {
91
91
  const poPatterns = path.join(dashboardPath, '**/*.po');
92
92
  const translations = await glob(poPatterns, {
93
93
  ignore: [
94
- // Standard test & doc files
95
- '**/node_modules/**/node_modules/**',
94
+ // Skip nested node_modules (transitive deps) but not .pnpm or .bun directories.
95
+ '**/node_modules/[!.]*/**/node_modules/**',
96
96
  '**/*.spec.js',
97
97
  '**/*.test.js',
98
98
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.5.3-master-202601300300",
4
+ "version": "3.5.3",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -157,8 +157,8 @@
157
157
  "@storybook/addon-vitest": "^10.0.0-beta.9",
158
158
  "@storybook/react-vite": "^10.0.0-beta.9",
159
159
  "@types/node": "^22.13.4",
160
- "@vendure/common": "^3.5.3-master-202601300300",
161
- "@vendure/core": "^3.5.3-master-202601300300",
160
+ "@vendure/common": "3.5.3",
161
+ "@vendure/core": "3.5.3",
162
162
  "@vitest/browser": "^3.2.4",
163
163
  "@vitest/coverage-v8": "^3.2.4",
164
164
  "eslint": "^9.19.0",
@@ -22,7 +22,9 @@ import {
22
22
  } from '@/vdb/framework/layout-engine/page-layout.js';
23
23
  import { detailPageRouteLoader } from '@/vdb/framework/page/detail-page-route-loader.js';
24
24
  import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
25
+ import { useJobQueuePolling } from '@/vdb/hooks/use-job-queue-polling.js';
25
26
  import { Trans, useLingui } from '@lingui/react/macro';
27
+ import { useQueryClient } from '@tanstack/react-query';
26
28
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
27
29
  import { toast } from 'sonner';
28
30
  import {
@@ -54,6 +56,12 @@ function CollectionDetailPage() {
54
56
  const navigate = useNavigate();
55
57
  const creatingNewEntity = params.id === NEW_ENTITY_PATH;
56
58
  const { t } = useLingui();
59
+ const queryClient = useQueryClient();
60
+
61
+ const { isPolling: pendingFilterApplication, startPolling } = useJobQueuePolling(
62
+ 'apply-collection-filters',
63
+ () => queryClient.invalidateQueries({ queryKey: ['PaginatedListDataTable'] }),
64
+ );
57
65
 
58
66
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
59
67
  pageId,
@@ -79,6 +87,7 @@ function CollectionDetailPage() {
79
87
  name: translation.name,
80
88
  slug: translation.slug,
81
89
  description: translation.description,
90
+ customFields: (translation as any).customFields,
82
91
  })),
83
92
  filters: entity.filters.map(f => ({
84
93
  code: f.code,
@@ -90,12 +99,20 @@ function CollectionDetailPage() {
90
99
  },
91
100
  params: { id: params.id },
92
101
  onSuccess: async data => {
102
+ const filtersWereDirty =
103
+ form.getFieldState('inheritFilters').isDirty || form.getFieldState('filters').isDirty;
93
104
  toast(
94
105
  creatingNewEntity ? t`Successfully created collection` : t`Successfully updated collection`,
95
106
  );
96
107
  resetForm();
108
+ if (filtersWereDirty) {
109
+ startPolling();
110
+ }
97
111
  if (creatingNewEntity) {
98
- await navigate({ to: `../$id`, params: { id: data.id } });
112
+ await navigate({
113
+ to: `../$id`,
114
+ params: { id: data.id },
115
+ });
99
116
  }
100
117
  },
101
118
  onError: err => {
@@ -106,7 +123,9 @@ function CollectionDetailPage() {
106
123
  });
107
124
 
108
125
  const shouldPreviewContents =
109
- form.getFieldState('inheritFilters').isDirty || form.getFieldState('filters').isDirty;
126
+ form.getFieldState('inheritFilters').isDirty ||
127
+ form.getFieldState('filters').isDirty ||
128
+ pendingFilterApplication;
110
129
 
111
130
  const currentFiltersValue = form.watch('filters');
112
131
  const currentInheritFiltersValue = form.watch('inheritFilters');
@@ -220,7 +239,7 @@ function CollectionDetailPage() {
220
239
  </FormItem>
221
240
  </PageBlock>
222
241
  <PageBlock column="main" blockId="contents" title={<Trans>Contents</Trans>}>
223
- {shouldPreviewContents || creatingNewEntity ? (
242
+ {pendingFilterApplication || shouldPreviewContents || creatingNewEntity ? (
224
243
  <CollectionContentsPreviewTable
225
244
  parentId={entity?.parent?.id}
226
245
  filters={currentFiltersValue ?? []}
@@ -10,6 +10,7 @@ export const customerListDocument = graphql(`
10
10
  firstName
11
11
  lastName
12
12
  emailAddress
13
+ phoneNumber
13
14
  groups {
14
15
  id
15
16
  name
@@ -30,6 +30,9 @@ function CustomerListPage() {
30
30
  emailAddress: {
31
31
  contains: searchTerm,
32
32
  },
33
+ phoneNumber: {
34
+ contains: searchTerm,
35
+ },
33
36
  };
34
37
  }}
35
38
  transformVariables={variables => {
@@ -0,0 +1,48 @@
1
+ import { Alert, AlertDescription, AlertTitle } from '@/vdb/components/ui/alert.js';
2
+ import { Trans, useLingui } from '@lingui/react/macro';
3
+ import { AlertTriangle, CheckCircle } from 'lucide-react';
4
+
5
+ export type DraftOrderStatusProps = Readonly<{
6
+ hasCustomer: boolean;
7
+ hasLines: boolean;
8
+ hasShippingMethod: boolean;
9
+ isDraftState: boolean;
10
+ }>;
11
+
12
+ export function DraftOrderStatus({
13
+ hasCustomer,
14
+ hasLines,
15
+ hasShippingMethod,
16
+ isDraftState,
17
+ }: DraftOrderStatusProps) {
18
+ const { t } = useLingui();
19
+ const isCompleteDraftDisabled = !hasCustomer || !hasLines || !hasShippingMethod || !isDraftState;
20
+
21
+ let completeDraftDisabledReason: string | null = null;
22
+ if (!hasCustomer) {
23
+ completeDraftDisabledReason = t`Select a customer to continue`;
24
+ } else if (!hasLines) {
25
+ completeDraftDisabledReason = t`Add at least one item to the order`;
26
+ } else if (!hasShippingMethod) {
27
+ completeDraftDisabledReason = t`Set a shipping address and select a shipping method`;
28
+ } else if (!isDraftState) {
29
+ completeDraftDisabledReason = t`Only draft orders can be completed`;
30
+ }
31
+
32
+ const Icon = isCompleteDraftDisabled ? AlertTriangle : CheckCircle;
33
+ const title = isCompleteDraftDisabled ? (
34
+ <Trans>Order draft isn't ready to be completed</Trans>
35
+ ) : (
36
+ <Trans>Order draft is ready to be completed</Trans>
37
+ );
38
+
39
+ return (
40
+ <Alert variant={isCompleteDraftDisabled ? 'destructive' : 'default'}>
41
+ <Icon className={isCompleteDraftDisabled ? '' : 'stroke-success'} />
42
+ <AlertTitle className={isCompleteDraftDisabled ? '' : 'text-success'}>{title}</AlertTitle>
43
+ {completeDraftDisabledReason ? (
44
+ <AlertDescription>{completeDraftDisabledReason}</AlertDescription>
45
+ ) : null}
46
+ </Alert>
47
+ );
48
+ }
@@ -24,6 +24,7 @@ import { ResultOf } from 'gql.tada';
24
24
  import { User } from 'lucide-react';
25
25
  import { toast } from 'sonner';
26
26
  import { CustomerAddressSelector } from './components/customer-address-selector.js';
27
+ import { DraftOrderStatus } from './components/draft-order-status.js';
27
28
  import { EditOrderTable } from './components/edit-order-table.js';
28
29
  import { OrderAddress } from './components/order-address.js';
29
30
  import {
@@ -289,6 +290,13 @@ function DraftOrderPage() {
289
290
  });
290
291
  };
291
292
 
293
+ const hasCustomer = !!entity.customer;
294
+ const hasLines = entity.lines.length > 0;
295
+ const hasShippingMethod = entity.shippingLines.length > 0;
296
+ const isDraftState = entity.state === 'Draft';
297
+
298
+ const isCompleteDraftDisabled = !hasCustomer || !hasLines || !hasShippingMethod || !isDraftState;
299
+
292
300
  return (
293
301
  <Page pageId="draft-order-detail" form={form} entity={entity}>
294
302
  <PageTitle>
@@ -312,12 +320,7 @@ function DraftOrderPage() {
312
320
  <PermissionGuard requires={['UpdateOrder']}>
313
321
  <Button
314
322
  type="button"
315
- disabled={
316
- !entity.customer ||
317
- entity.lines.length === 0 ||
318
- entity.shippingLines.length === 0 ||
319
- entity.state !== 'Draft'
320
- }
323
+ disabled={isCompleteDraftDisabled}
321
324
  onClick={() => completeDraftOrder({ id: entity.id, state: 'ArrangingPayment' })}
322
325
  >
323
326
  <Trans>Complete draft</Trans>
@@ -325,7 +328,20 @@ function DraftOrderPage() {
325
328
  </PermissionGuard>
326
329
  </PageActionBarRight>
327
330
  </PageActionBar>
331
+
328
332
  <PageLayout>
333
+ <PageBlock
334
+ column="side"
335
+ blockId="draft-order-status"
336
+ title={<Trans>Draft order status</Trans>}
337
+ >
338
+ <DraftOrderStatus
339
+ hasCustomer={hasCustomer}
340
+ hasLines={hasLines}
341
+ hasShippingMethod={hasShippingMethod}
342
+ isDraftState={isDraftState}
343
+ />
344
+ </PageBlock>
329
345
  <PageBlock column="main" blockId="order-table">
330
346
  <EditOrderTable
331
347
  order={entity}
@@ -82,6 +82,7 @@ function PaymentMethodDetailPage() {
82
82
  languageCode: translation.languageCode,
83
83
  name: translation.name,
84
84
  description: translation.description,
85
+ customFields: (translation as any).customFields,
85
86
  })),
86
87
  customFields: entity.customFields,
87
88
  };
@@ -32,6 +32,13 @@ export const productVariantListDocument = graphql(
32
32
  [assetFragment],
33
33
  );
34
34
 
35
+ export const productVariantPriceFragment = graphql(`
36
+ fragment ProductVariantPrice on ProductVariantPrice {
37
+ currencyCode
38
+ price
39
+ }
40
+ `);
41
+
35
42
  export const productVariantDetailDocument = graphql(
36
43
  `
37
44
  query ProductVariantDetail($id: ID!) {
@@ -86,8 +93,7 @@ export const productVariantDetailDocument = graphql(
86
93
  price
87
94
  priceWithTax
88
95
  prices {
89
- currencyCode
90
- price
96
+ ...ProductVariantPrice
91
97
  }
92
98
  trackInventory
93
99
  outOfStockThreshold
@@ -105,7 +111,7 @@ export const productVariantDetailDocument = graphql(
105
111
  }
106
112
  }
107
113
  `,
108
- [assetFragment],
114
+ [assetFragment, productVariantPriceFragment],
109
115
  );
110
116
 
111
117
  export const createProductVariantDocument = graphql(`
@@ -1,6 +1,7 @@
1
1
  import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
2
2
  import { NumberInput } from '@/vdb/components/data-input/number-input.js';
3
3
  import { AssignedFacetValues } from '@/vdb/components/shared/assigned-facet-values.js';
4
+ import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
4
5
  import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
5
6
  import { EntityAssets } from '@/vdb/components/shared/entity-assets.js';
6
7
  import { ErrorPage } from '@/vdb/components/shared/error-page.js';
@@ -12,8 +13,10 @@ import { Button } from '@/vdb/components/ui/button.js';
12
13
  import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@/vdb/components/ui/form.js';
13
14
  import { Input } from '@/vdb/components/ui/input.js';
14
15
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
16
+ import { Separator } from '@/vdb/components/ui/separator.js';
15
17
  import { Switch } from '@/vdb/components/ui/switch.js';
16
18
  import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
19
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
17
20
  import {
18
21
  CustomFieldsPageBlock,
19
22
  DetailFormGrid,
@@ -34,6 +37,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router';
34
37
  import { VariablesOf } from 'gql.tada';
35
38
  import { Trash } from 'lucide-react';
36
39
  import { toast } from 'sonner';
40
+
37
41
  import { AddCurrencyDropdown } from './components/add-currency-dropdown.js';
38
42
  import { AddStockLocationDropdown } from './components/add-stock-location-dropdown.js';
39
43
  import { VariantPriceDetail } from './components/variant-price-detail.js';
@@ -50,7 +54,10 @@ export const Route = createFileRoute('/_authenticated/_product-variants/product-
50
54
  component: ProductVariantDetailPage,
51
55
  loader: detailPageRouteLoader({
52
56
  pageId,
53
- queryDocument: productVariantDetailDocument,
57
+ queryDocument: () =>
58
+ addCustomFields(productVariantDetailDocument, {
59
+ includeNestedFragments: ['ProductVariantPrice'],
60
+ }),
54
61
  breadcrumb(_isNew, entity, location) {
55
62
  if ((location.search as any).from === 'product') {
56
63
  return [
@@ -81,7 +88,9 @@ function ProductVariantDetailPage() {
81
88
 
82
89
  const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
83
90
  pageId,
84
- queryDocument: productVariantDetailDocument,
91
+ queryDocument: addCustomFields(productVariantDetailDocument, {
92
+ includeNestedFragments: ['ProductVariantPrice'],
93
+ }),
85
94
  createDocument: createProductVariantDocument,
86
95
  updateDocument: updateProductVariantDocument,
87
96
  setValuesForUpdate: entity => {
@@ -169,6 +178,7 @@ function ProductVariantDetailPage() {
169
178
  currencyCode,
170
179
  price: 0,
171
180
  delete: false,
181
+ customFields: {},
172
182
  } as PriceInput;
173
183
  form.setValue('prices', [...currentPrices, newPrice], {
174
184
  shouldDirty: true,
@@ -274,37 +284,46 @@ function ProductVariantDetailPage() {
274
284
  </div>
275
285
  );
276
286
  return (
277
- <DetailFormGrid key={price.currencyCode}>
278
- <div className="flex gap-1 items-end">
279
- <FormFieldWrapper
280
- control={form.control}
281
- name={`prices.${actualIndex}.price`}
282
- label={priceLabel}
283
- render={({ field }) => (
284
- <MoneyInput {...field} currency={price.currencyCode} />
287
+ <div key={price.currencyCode} className="space-y-6">
288
+ {displayIndex > 0 && <Separator className="my-4" />}
289
+ <DetailFormGrid key={price.currencyCode}>
290
+ <div className="flex gap-1 items-end">
291
+ <FormFieldWrapper
292
+ control={form.control}
293
+ name={`prices.${actualIndex}.price`}
294
+ label={priceLabel}
295
+ render={({ field }) => (
296
+ <MoneyInput {...field} currency={price.currencyCode} />
297
+ )}
298
+ />
299
+ {activePrices.length > 1 && (
300
+ <Button
301
+ type="button"
302
+ variant="ghost"
303
+ size="sm"
304
+ onClick={() => handleRemoveCurrency(actualIndex)}
305
+ className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
306
+ >
307
+ <Trash className="size-4" />
308
+ </Button>
285
309
  )}
310
+ </div>
311
+ <VariantPriceDetail
312
+ priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
313
+ price={price.price}
314
+ currencyCode={
315
+ price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
316
+ }
317
+ taxCategoryId={taxCategoryId}
286
318
  />
287
- {activePrices.length > 1 && (
288
- <Button
289
- type="button"
290
- variant="ghost"
291
- size="sm"
292
- onClick={() => handleRemoveCurrency(actualIndex)}
293
- className="h-6 w-6 p-0 mb-2 hover:text-destructive hover:bg-destructive-100"
294
- >
295
- <Trash className="size-4" />
296
- </Button>
297
- )}
298
- </div>
299
- <VariantPriceDetail
300
- priceIncludesTax={activeChannel?.pricesIncludeTax ?? false}
301
- price={price.price}
302
- currencyCode={
303
- price.currencyCode ?? activeChannel?.defaultCurrencyCode ?? ''
304
- }
305
- taxCategoryId={taxCategoryId}
319
+ </DetailFormGrid>
320
+ {/* Custom fields for ProductVariantPrice */}
321
+ <CustomFieldsForm
322
+ entityType="ProductVariantPrice"
323
+ control={form.control}
324
+ formPathPrefix={`prices.${actualIndex}`}
306
325
  />
307
- </DetailFormGrid>
326
+ </div>
308
327
  );
309
328
  })}
310
329
  {unusedCurrencies.length ? (
@@ -93,6 +93,7 @@ function PromotionDetailPage() {
93
93
  languageCode: translation.languageCode,
94
94
  name: translation.name,
95
95
  description: translation.description,
96
+ customFields: (translation as any).customFields,
96
97
  })),
97
98
  customFields: entity.customFields,
98
99
  };
@@ -79,6 +79,7 @@ function ShippingMethodDetailPage() {
79
79
  languageCode: translation.languageCode,
80
80
  name: translation.name,
81
81
  description: translation.description,
82
+ customFields: (translation as any).customFields,
82
83
  })),
83
84
  customFields: entity.customFields,
84
85
  };