@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.
- package/dist/vite/utils/plugin-discovery.js +3 -3
- package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
- package/dist/vite/vite-plugin-lingui-babel.js +90 -8
- package/dist/vite/vite-plugin-translations.js +2 -2
- package/package.json +3 -3
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +22 -3
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
- package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -0
- package/src/i18n/locales/ar.po +58 -5
- package/src/i18n/locales/en.po +58 -5
- package/src/lib/components/data-input/index.ts +1 -0
- package/src/lib/components/ui/alert.tsx +2 -0
- package/src/lib/framework/extension-api/input-component-extensions.tsx +2 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
- package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
- package/src/lib/framework/page/detail-page.tsx +22 -37
- package/src/lib/graphql/graphql-env.d.ts +30 -13
- 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
|
|
327
|
-
// [!.]
|
|
328
|
-
'**/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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
95
|
-
'**/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
|
|
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": "
|
|
161
|
-
"@vendure/core": "
|
|
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({
|
|
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 ||
|
|
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 ?? []}
|
|
@@ -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}
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
<
|
|
278
|
-
<
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
</
|
|
326
|
+
</div>
|
|
308
327
|
);
|
|
309
328
|
})}
|
|
310
329
|
{unusedCurrencies.length ? (
|