@vendure/dashboard 3.3.5-master-202506250724 → 3.3.5-master-202506251305
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/plugin/tests/barrel-exports.spec.js +1 -1
- package/dist/plugin/vite-plugin-config.js +1 -0
- package/dist/plugin/vite-plugin-dashboard-metadata.d.ts +1 -3
- package/dist/plugin/vite-plugin-dashboard-metadata.js +1 -8
- package/dist/plugin/vite-plugin-tailwind-source.d.ts +7 -0
- package/dist/plugin/vite-plugin-tailwind-source.js +49 -0
- package/dist/plugin/vite-plugin-vendure-dashboard.js +3 -1
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +43 -34
- package/src/app/routes/_authenticated/_channels/channels_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_countries/countries_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_customer-groups/customer-groups_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_customers/customers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +2 -5
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_products/components/assign-facet-values-dialog.tsx +98 -0
- package/src/app/routes/_authenticated/_products/components/assign-to-channel-dialog.tsx +126 -0
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +268 -0
- package/src/app/routes/_authenticated/_products/products.graphql.ts +64 -0
- package/src/app/routes/_authenticated/_products/products.tsx +31 -2
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +14 -9
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_sellers/sellers_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_stock-locations/stock-locations_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_tax-categories/tax-categories_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -1
- package/src/app/routes/_authenticated/_zones/zones_.$id.tsx +1 -1
- package/src/app/styles.css +3 -0
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +101 -0
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +89 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +16 -8
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +4 -4
- package/src/lib/components/data-table/data-table-pagination.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +50 -31
- package/src/lib/components/data-table/human-readable-operator.tsx +3 -3
- package/src/lib/components/shared/assigned-facet-values.tsx +1 -5
- package/src/lib/components/shared/paginated-list-data-table.tsx +47 -11
- package/src/lib/framework/data-table/data-table-extensions.ts +21 -0
- package/src/lib/framework/data-table/data-table-types.ts +25 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +11 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +35 -0
- package/src/lib/framework/form-engine/use-generated-form.tsx +2 -5
- package/src/lib/framework/layout-engine/page-block-provider.tsx +6 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +43 -33
- package/src/lib/framework/page/list-page.tsx +6 -8
- package/src/lib/framework/registry/registry-types.ts +4 -2
- package/src/lib/hooks/use-page-block.tsx +10 -0
- package/src/lib/index.ts +8 -1
- package/vite/tests/barrel-exports.spec.ts +13 -9
- package/vite/vite-plugin-config.ts +1 -0
- package/vite/vite-plugin-dashboard-metadata.ts +1 -9
- package/vite/vite-plugin-tailwind-source.ts +65 -0
- package/vite/vite-plugin-vendure-dashboard.ts +5 -3
- /package/src/lib/components/data-table/{data-table-types.ts → types.ts} +0 -0
|
@@ -10,5 +10,5 @@ describe('detecting plugins in barrel exports', () => {
|
|
|
10
10
|
expect(result.pluginInfo).toHaveLength(1);
|
|
11
11
|
expect(result.pluginInfo[0].name).toBe('MyPlugin');
|
|
12
12
|
expect(result.pluginInfo[0].dashboardEntryPath).toBe('./dashboard/index.tsx');
|
|
13
|
-
});
|
|
13
|
+
}, { timeout: 10000 });
|
|
14
14
|
});
|
|
@@ -49,6 +49,7 @@ export function viteConfigPlugin({ packageRoot }) {
|
|
|
49
49
|
...(((_e = config.optimizeDeps) === null || _e === void 0 ? void 0 : _e.include) || []),
|
|
50
50
|
'@/components > recharts',
|
|
51
51
|
'@/components > react-dropzone',
|
|
52
|
+
'@vendure/common/lib/generated-types',
|
|
52
53
|
] });
|
|
53
54
|
return config;
|
|
54
55
|
},
|
|
@@ -4,6 +4,4 @@ import { Plugin } from 'vite';
|
|
|
4
4
|
* generates an import statement for each one, wrapped up in a `runDashboardExtensions()`
|
|
5
5
|
* function which can then be imported and executed in the Dashboard app.
|
|
6
6
|
*/
|
|
7
|
-
export declare function dashboardMetadataPlugin(
|
|
8
|
-
rootDir: string;
|
|
9
|
-
}): Plugin;
|
|
7
|
+
export declare function dashboardMetadataPlugin(): Plugin;
|
|
@@ -7,7 +7,7 @@ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
|
7
7
|
* generates an import statement for each one, wrapped up in a `runDashboardExtensions()`
|
|
8
8
|
* function which can then be imported and executed in the Dashboard app.
|
|
9
9
|
*/
|
|
10
|
-
export function dashboardMetadataPlugin(
|
|
10
|
+
export function dashboardMetadataPlugin() {
|
|
11
11
|
let configLoaderApi;
|
|
12
12
|
let loadVendureConfigResult;
|
|
13
13
|
return {
|
|
@@ -41,10 +41,3 @@ export function dashboardMetadataPlugin(options) {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Converts an import path to a normalized path relative to the rootDir.
|
|
46
|
-
*/
|
|
47
|
-
function normalizeImportPath(rootDir, importPath) {
|
|
48
|
-
const relativePath = path.relative(rootDir, importPath).replace(/\\/g, '/');
|
|
49
|
-
return relativePath.replace(/\.tsx?$/, '.js');
|
|
50
|
-
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
/**
|
|
3
|
+
* This Vite plugin transforms the `app/styles.css` file to include a `@source` directive
|
|
4
|
+
* for each dashboard extension's source directory. This allows Tailwind CSS to
|
|
5
|
+
* include styles from these extensions when processing the CSS.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dashboardTailwindSourcePlugin(): Plugin;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* This Vite plugin transforms the `app/styles.css` file to include a `@source` directive
|
|
5
|
+
* for each dashboard extension's source directory. This allows Tailwind CSS to
|
|
6
|
+
* include styles from these extensions when processing the CSS.
|
|
7
|
+
*/
|
|
8
|
+
export function dashboardTailwindSourcePlugin() {
|
|
9
|
+
let configLoaderApi;
|
|
10
|
+
let loadVendureConfigResult;
|
|
11
|
+
return {
|
|
12
|
+
name: 'vendure:dashboard-tailwind-source',
|
|
13
|
+
// Ensure this plugin runs before Tailwind CSS processing
|
|
14
|
+
enforce: 'pre',
|
|
15
|
+
configResolved({ plugins }) {
|
|
16
|
+
configLoaderApi = getConfigLoaderApi(plugins);
|
|
17
|
+
},
|
|
18
|
+
async transform(src, id) {
|
|
19
|
+
var _a;
|
|
20
|
+
if (/app\/styles.css$/.test(id)) {
|
|
21
|
+
if (!loadVendureConfigResult) {
|
|
22
|
+
loadVendureConfigResult = await configLoaderApi.getVendureConfig();
|
|
23
|
+
}
|
|
24
|
+
const { pluginInfo } = loadVendureConfigResult;
|
|
25
|
+
const dashboardExtensionDirs = (_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.map(({ dashboardEntryPath, pluginPath }) => dashboardEntryPath && path.join(pluginPath, path.dirname(dashboardEntryPath))).filter(x => x != null)) !== null && _a !== void 0 ? _a : [];
|
|
26
|
+
const sources = dashboardExtensionDirs
|
|
27
|
+
.map(extension => {
|
|
28
|
+
return `@source '${extension}';`;
|
|
29
|
+
})
|
|
30
|
+
.join('\n');
|
|
31
|
+
// Find the line with the specific comment and insert sources after it
|
|
32
|
+
const lines = src.split('\n');
|
|
33
|
+
const sourceCommentIndex = lines.findIndex(line => line.includes('/* @source rules from extensions will be added here by the dashboardTailwindSourcePlugin */'));
|
|
34
|
+
if (sourceCommentIndex !== -1) {
|
|
35
|
+
// Insert the sources after the comment line
|
|
36
|
+
lines.splice(sourceCommentIndex + 1, 0, sources);
|
|
37
|
+
const modifiedSrc = lines.join('\n');
|
|
38
|
+
return {
|
|
39
|
+
code: modifiedSrc,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// If the comment is not found, append sources at the end
|
|
43
|
+
return {
|
|
44
|
+
code: src + '\n' + sources,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -7,6 +7,7 @@ import { configLoaderPlugin } from './vite-plugin-config-loader.js';
|
|
|
7
7
|
import { viteConfigPlugin } from './vite-plugin-config.js';
|
|
8
8
|
import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
|
|
9
9
|
import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
|
|
10
|
+
import { dashboardTailwindSourcePlugin } from './vite-plugin-tailwind-source.js';
|
|
10
11
|
import { themeVariablesPlugin } from './vite-plugin-theme.js';
|
|
11
12
|
import { transformIndexHtmlPlugin } from './vite-plugin-transform-index.js';
|
|
12
13
|
import { uiConfigPlugin } from './vite-plugin-ui-config.js';
|
|
@@ -42,6 +43,7 @@ export function vendureDashboardPlugin(options) {
|
|
|
42
43
|
// },
|
|
43
44
|
}),
|
|
44
45
|
themeVariablesPlugin({ theme: options.theme }),
|
|
46
|
+
dashboardTailwindSourcePlugin(),
|
|
45
47
|
tailwindcss(),
|
|
46
48
|
configLoaderPlugin({
|
|
47
49
|
vendureConfigPath: normalizedVendureConfigPath,
|
|
@@ -51,7 +53,7 @@ export function vendureDashboardPlugin(options) {
|
|
|
51
53
|
}),
|
|
52
54
|
viteConfigPlugin({ packageRoot }),
|
|
53
55
|
adminApiSchemaPlugin(),
|
|
54
|
-
dashboardMetadataPlugin(
|
|
56
|
+
dashboardMetadataPlugin(),
|
|
55
57
|
uiConfigPlugin({ adminUiConfig: options.adminUiConfig }),
|
|
56
58
|
...(options.gqlTadaOutputPath
|
|
57
59
|
? [gqlTadaPlugin({ gqlTadaOutputPath: options.gqlTadaOutputPath, tempDir, packageRoot })]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.5-master-
|
|
4
|
+
"version": "3.3.5-master-202506251305",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/react-dom": "^19.0.4",
|
|
87
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
88
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
89
|
-
"@vendure/common": "^3.3.5-master-
|
|
90
|
-
"@vendure/core": "^3.3.5-master-
|
|
89
|
+
"@vendure/common": "^3.3.5-master-202506251305",
|
|
90
|
+
"@vendure/core": "^3.3.5-master-202506251305",
|
|
91
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
92
92
|
"awesome-graphql-client": "^2.1.0",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
131
131
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
132
132
|
},
|
|
133
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "6781f6697a37aa8c7b7e76e9fc53b4dd0300a94b"
|
|
134
134
|
}
|
|
@@ -87,7 +87,7 @@ function AdministratorDetailPage() {
|
|
|
87
87
|
const roleIds = form.watch('roleIds');
|
|
88
88
|
|
|
89
89
|
return (
|
|
90
|
-
<Page pageId="administrator-detail" form={form} submitHandler={submitHandler}>
|
|
90
|
+
<Page pageId="administrator-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
91
91
|
<PageTitle>{creatingNewEntity ? <Trans>New administrator</Trans> : name}</PageTitle>
|
|
92
92
|
|
|
93
93
|
<PageActionBar>
|
|
@@ -1,23 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { detailPageRouteLoader } from '@/framework/page/detail-page-route-loader.js';
|
|
3
|
-
import { createFileRoute } from '@tanstack/react-router'
|
|
4
|
-
import { assetDetailDocument, assetUpdateDocument } from './assets.graphql.js';
|
|
5
|
-
import { Trans, useLingui } from '@/lib/trans.js';
|
|
6
|
-
import { ErrorPage } from '@/components/shared/error-page.js';
|
|
7
|
-
import { toast } from 'sonner';
|
|
8
|
-
import { Page, PageTitle, PageActionBar, PageActionBarRight, PageBlock, PageLayout, CustomFieldsPageBlock } from '@/framework/layout-engine/page-layout.js'
|
|
9
|
-
import { useDetailPage } from '@/framework/page/use-detail-page.js';
|
|
10
|
-
import { PermissionGuard } from '@/components/shared/permission-guard.js';
|
|
11
|
-
import { Button } from '@/components/ui/button.js';
|
|
12
|
-
import { VendureImage } from '@/components/shared/vendure-image.js';
|
|
13
|
-
import { useState, useRef } from 'react';
|
|
14
|
-
import { PreviewPreset } from '@/components/shared/asset/asset-preview.js';
|
|
1
|
+
import { AssetFocalPointEditor } from '@/components/shared/asset/asset-focal-point-editor.js';
|
|
15
2
|
import { AssetPreviewSelector } from '@/components/shared/asset/asset-preview-selector.js';
|
|
3
|
+
import { PreviewPreset } from '@/components/shared/asset/asset-preview.js';
|
|
16
4
|
import { AssetProperties } from '@/components/shared/asset/asset-properties.js';
|
|
17
|
-
import { AssetFocalPointEditor } from '@/components/shared/asset/asset-focal-point-editor.js';
|
|
18
|
-
import { FocusIcon } from 'lucide-react';
|
|
19
5
|
import { Point } from '@/components/shared/asset/focal-point-control.js';
|
|
6
|
+
import { ErrorPage } from '@/components/shared/error-page.js';
|
|
7
|
+
import { PermissionGuard } from '@/components/shared/permission-guard.js';
|
|
8
|
+
import { VendureImage } from '@/components/shared/vendure-image.js';
|
|
9
|
+
import { Button } from '@/components/ui/button.js';
|
|
20
10
|
import { Label } from '@/components/ui/label.js';
|
|
11
|
+
import {
|
|
12
|
+
CustomFieldsPageBlock,
|
|
13
|
+
Page,
|
|
14
|
+
PageActionBar,
|
|
15
|
+
PageActionBarRight,
|
|
16
|
+
PageBlock,
|
|
17
|
+
PageLayout,
|
|
18
|
+
PageTitle,
|
|
19
|
+
} from '@/framework/layout-engine/page-layout.js';
|
|
20
|
+
import { detailPageRouteLoader } from '@/framework/page/detail-page-route-loader.js';
|
|
21
|
+
import { useDetailPage } from '@/framework/page/use-detail-page.js';
|
|
22
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
23
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
24
|
+
import { FocusIcon } from 'lucide-react';
|
|
25
|
+
import { useRef, useState } from 'react';
|
|
26
|
+
import { toast } from 'sonner';
|
|
27
|
+
import { assetDetailDocument, assetUpdateDocument } from './assets.graphql.js';
|
|
21
28
|
export const Route = createFileRoute('/_authenticated/_assets/assets_/$id')({
|
|
22
29
|
component: AssetDetailPage,
|
|
23
30
|
loader: detailPageRouteLoader({
|
|
@@ -25,7 +32,7 @@ export const Route = createFileRoute('/_authenticated/_assets/assets_/$id')({
|
|
|
25
32
|
breadcrumb(isNew, entity) {
|
|
26
33
|
return [
|
|
27
34
|
{ path: '/assets', label: 'Assets' },
|
|
28
|
-
isNew ? <Trans>New asset</Trans> : entity?.name ?? '',
|
|
35
|
+
isNew ? <Trans>New asset</Trans> : (entity?.name ?? ''),
|
|
29
36
|
];
|
|
30
37
|
},
|
|
31
38
|
}),
|
|
@@ -79,17 +86,14 @@ function AssetDetailPage() {
|
|
|
79
86
|
return null;
|
|
80
87
|
}
|
|
81
88
|
return (
|
|
82
|
-
<Page pageId="asset-detail" form={form} submitHandler={submitHandler}>
|
|
89
|
+
<Page pageId="asset-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
83
90
|
<PageTitle>
|
|
84
91
|
<Trans>Edit asset</Trans>
|
|
85
92
|
</PageTitle>
|
|
86
93
|
<PageActionBar>
|
|
87
94
|
<PageActionBarRight>
|
|
88
95
|
<PermissionGuard requires={['UpdateChannel']}>
|
|
89
|
-
<Button
|
|
90
|
-
type="submit"
|
|
91
|
-
disabled={!form.formState.isDirty || isPending}
|
|
92
|
-
>
|
|
96
|
+
<Button type="submit" disabled={!form.formState.isDirty || isPending}>
|
|
93
97
|
<Trans>Update</Trans>
|
|
94
98
|
</Button>
|
|
95
99
|
</PermissionGuard>
|
|
@@ -103,7 +107,7 @@ function AssetDetailPage() {
|
|
|
103
107
|
height={height}
|
|
104
108
|
settingFocalPoint={settingFocalPoint}
|
|
105
109
|
focalPoint={form.getValues().focalPoint ?? { x: 0.5, y: 0.5 }}
|
|
106
|
-
onFocalPointChange={
|
|
110
|
+
onFocalPointChange={point => {
|
|
107
111
|
form.setValue('focalPoint.x', point.x, { shouldDirty: true });
|
|
108
112
|
form.setValue('focalPoint.y', point.y, { shouldDirty: true });
|
|
109
113
|
setSettingFocalPoint(false);
|
|
@@ -124,11 +128,7 @@ function AssetDetailPage() {
|
|
|
124
128
|
</AssetFocalPointEditor>
|
|
125
129
|
</div>
|
|
126
130
|
</PageBlock>
|
|
127
|
-
<CustomFieldsPageBlock
|
|
128
|
-
column="main"
|
|
129
|
-
entityType={'Asset'}
|
|
130
|
-
control={form.control}
|
|
131
|
-
/>
|
|
131
|
+
<CustomFieldsPageBlock column="main" entityType={'Asset'} control={form.control} />
|
|
132
132
|
<PageBlock column="side" blockId="asset-properties">
|
|
133
133
|
<AssetProperties asset={entity} />
|
|
134
134
|
</PageBlock>
|
|
@@ -136,15 +136,24 @@ function AssetDetailPage() {
|
|
|
136
136
|
<div className="flex flex-col gap-2">
|
|
137
137
|
<AssetPreviewSelector size={size} setSize={setSize} width={width} height={height} />
|
|
138
138
|
<div className="flex items-center gap-2">
|
|
139
|
-
<Button
|
|
139
|
+
<Button
|
|
140
|
+
type="button"
|
|
141
|
+
variant="outline"
|
|
142
|
+
size="icon"
|
|
143
|
+
onClick={() => setSettingFocalPoint(true)}
|
|
144
|
+
>
|
|
140
145
|
<FocusIcon className="h-4 w-4" />
|
|
141
146
|
</Button>
|
|
142
147
|
<div className="text-sm text-muted-foreground">
|
|
143
|
-
<Label
|
|
148
|
+
<Label>
|
|
149
|
+
<Trans>Focal Point</Trans>
|
|
150
|
+
</Label>
|
|
144
151
|
<div className="text-sm text-muted-foreground">
|
|
145
|
-
{form.getValues().focalPoint?.x && form.getValues().focalPoint?.y
|
|
146
|
-
|
|
147
|
-
|
|
152
|
+
{form.getValues().focalPoint?.x && form.getValues().focalPoint?.y ? (
|
|
153
|
+
`${form.getValues().focalPoint?.x.toFixed(2)}, ${form.getValues().focalPoint?.y.toFixed(2)}`
|
|
154
|
+
) : (
|
|
155
|
+
<Trans>Not set</Trans>
|
|
156
|
+
)}
|
|
148
157
|
</div>
|
|
149
158
|
</div>
|
|
150
159
|
</div>
|
|
@@ -152,5 +161,5 @@ function AssetDetailPage() {
|
|
|
152
161
|
</PageBlock>
|
|
153
162
|
</PageLayout>
|
|
154
163
|
</Page>
|
|
155
|
-
)
|
|
164
|
+
);
|
|
156
165
|
}
|
|
@@ -100,7 +100,7 @@ function ChannelDetailPage() {
|
|
|
100
100
|
const codeIsDefault = entity?.code === DEFAULT_CHANNEL_CODE;
|
|
101
101
|
|
|
102
102
|
return (
|
|
103
|
-
<Page pageId="channel-detail" form={form} submitHandler={submitHandler}>
|
|
103
|
+
<Page pageId="channel-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
104
104
|
<PageTitle>
|
|
105
105
|
{creatingNewEntity ? (
|
|
106
106
|
<Trans>New channel</Trans>
|
|
@@ -105,7 +105,7 @@ function CollectionDetailPage() {
|
|
|
105
105
|
const currentInheritFiltersValue = form.watch('inheritFilters');
|
|
106
106
|
|
|
107
107
|
return (
|
|
108
|
-
<Page pageId="collection-detail" form={form} submitHandler={submitHandler}>
|
|
108
|
+
<Page pageId="collection-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
109
109
|
<PageTitle>{creatingNewEntity ? <Trans>New collection</Trans> : (entity?.name ?? '')}</PageTitle>
|
|
110
110
|
<PageActionBar>
|
|
111
111
|
<PageActionBarRight>
|
|
@@ -71,7 +71,7 @@ function CountryDetailPage() {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
|
-
<Page pageId="country-detail" form={form} submitHandler={submitHandler}>
|
|
74
|
+
<Page pageId="country-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
75
75
|
<PageTitle>{creatingNewEntity ? <Trans>New country</Trans> : (entity?.name ?? '')}</PageTitle>
|
|
76
76
|
<PageActionBar>
|
|
77
77
|
<PageActionBarRight>
|
|
@@ -72,7 +72,7 @@ function CustomerGroupDetailPage() {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
|
-
<Page pageId="customer-group-detail" form={form} submitHandler={submitHandler}>
|
|
75
|
+
<Page pageId="customer-group-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
76
76
|
<PageTitle>
|
|
77
77
|
{creatingNewEntity ? <Trans>New customer group</Trans> : (entity?.name ?? '')}
|
|
78
78
|
</PageTitle>
|
|
@@ -137,7 +137,7 @@ function CustomerDetailPage() {
|
|
|
137
137
|
const customerName = entity ? `${entity.firstName} ${entity.lastName}` : '';
|
|
138
138
|
|
|
139
139
|
return (
|
|
140
|
-
<Page pageId="customer-detail" form={form} submitHandler={submitHandler}>
|
|
140
|
+
<Page pageId="customer-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
141
141
|
<PageTitle>{creatingNewEntity ? <Trans>New customer</Trans> : customerName}</PageTitle>
|
|
142
142
|
<PageActionBar>
|
|
143
143
|
<PageActionBarRight>
|
|
@@ -82,7 +82,7 @@ function FacetDetailPage() {
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
return (
|
|
85
|
-
<Page pageId="facet-detail" form={form} submitHandler={submitHandler}>
|
|
85
|
+
<Page pageId="facet-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
86
86
|
<PageTitle>{creatingNewEntity ? <Trans>New facet</Trans> : (entity?.name ?? '')}</PageTitle>
|
|
87
87
|
<PageActionBar>
|
|
88
88
|
<PageActionBarRight>
|
|
@@ -78,7 +78,7 @@ function GlobalSettingsPage() {
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
|
-
<Page pageId="global-settings" form={form} submitHandler={submitHandler}>
|
|
81
|
+
<Page pageId="global-settings" form={form} submitHandler={submitHandler} entity={entity}>
|
|
82
82
|
<PageTitle>
|
|
83
83
|
<Trans>Global settings</Trans>
|
|
84
84
|
</PageTitle>
|
|
@@ -27,10 +27,7 @@ import { orderDetailDocument } from './orders.graphql.js';
|
|
|
27
27
|
|
|
28
28
|
export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
|
|
29
29
|
component: OrderDetailPage,
|
|
30
|
-
loader: async ({
|
|
31
|
-
context,
|
|
32
|
-
params,
|
|
33
|
-
}) => {
|
|
30
|
+
loader: async ({ context, params }) => {
|
|
34
31
|
if (!params.id) {
|
|
35
32
|
throw new Error('ID param is required');
|
|
36
33
|
}
|
|
@@ -86,7 +83,7 @@ function OrderDetailPage() {
|
|
|
86
83
|
}
|
|
87
84
|
|
|
88
85
|
return (
|
|
89
|
-
<Page pageId="order-detail" form={form} submitHandler={submitHandler}>
|
|
86
|
+
<Page pageId="order-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
90
87
|
<PageTitle>{entity?.code ?? ''}</PageTitle>
|
|
91
88
|
<PageActionBar>
|
|
92
89
|
<PageActionBarRight>
|
|
@@ -102,7 +102,7 @@ function PaymentMethodDetailPage() {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
return (
|
|
105
|
-
<Page pageId="payment-method-detail" form={form} submitHandler={submitHandler}>
|
|
105
|
+
<Page pageId="payment-method-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
106
106
|
<PageTitle>
|
|
107
107
|
{creatingNewEntity ? <Trans>New payment method</Trans> : (entity?.name ?? '')}
|
|
108
108
|
</PageTitle>
|
|
@@ -102,7 +102,7 @@ function ProductVariantDetailPage() {
|
|
|
102
102
|
const [price, taxCategoryId] = form.watch(['price', 'taxCategoryId']);
|
|
103
103
|
|
|
104
104
|
return (
|
|
105
|
-
<Page pageId="product-variant-detail" form={form} submitHandler={submitHandler}>
|
|
105
|
+
<Page pageId="product-variant-detail" form={form} submitHandler={submitHandler} entity={entity}>
|
|
106
106
|
<PageTitle>
|
|
107
107
|
{creatingNewEntity ? <Trans>New product variant</Trans> : (entity?.name ?? '')}
|
|
108
108
|
</PageTitle>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { toast } from 'sonner';
|
|
3
|
+
import { useMutation } from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button.js';
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from '@/components/ui/dialog.js';
|
|
14
|
+
import { FacetValueSelector, FacetValue } from '@/components/shared/facet-value-selector.js';
|
|
15
|
+
import { api } from '@/graphql/api.js';
|
|
16
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
17
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
18
|
+
|
|
19
|
+
import { updateProductsDocument } from '../products.graphql.js';
|
|
20
|
+
|
|
21
|
+
interface AssignFacetValuesDialogProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onOpenChange: (open: boolean) => void;
|
|
24
|
+
productIds: string[];
|
|
25
|
+
onSuccess?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function AssignFacetValuesDialog({ open, onOpenChange, productIds, onSuccess }: AssignFacetValuesDialogProps) {
|
|
29
|
+
const { i18n } = useLingui();
|
|
30
|
+
const [selectedFacetValueIds, setSelectedFacetValueIds] = useState<string[]>([]);
|
|
31
|
+
|
|
32
|
+
const { mutate, isPending } = useMutation({
|
|
33
|
+
mutationFn: api.mutate(updateProductsDocument),
|
|
34
|
+
onSuccess: (result: ResultOf<typeof updateProductsDocument>) => {
|
|
35
|
+
toast.success(i18n.t(`Successfully updated facet values for ${productIds.length} products`));
|
|
36
|
+
onSuccess?.();
|
|
37
|
+
onOpenChange(false);
|
|
38
|
+
},
|
|
39
|
+
onError: () => {
|
|
40
|
+
toast.error(`Failed to update facet values for ${productIds.length} products`);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const handleAssign = () => {
|
|
45
|
+
if (selectedFacetValueIds.length === 0) {
|
|
46
|
+
toast.error('Please select at least one facet value');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
mutate({
|
|
51
|
+
input: productIds.map(productId => ({
|
|
52
|
+
id: productId,
|
|
53
|
+
facetValueIds: selectedFacetValueIds,
|
|
54
|
+
})),
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleFacetValueSelect = (facetValue: FacetValue) => {
|
|
59
|
+
setSelectedFacetValueIds(prev => [...new Set([...prev, facetValue.id])]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
64
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
65
|
+
<DialogHeader>
|
|
66
|
+
<DialogTitle><Trans>Edit facet values</Trans></DialogTitle>
|
|
67
|
+
<DialogDescription>
|
|
68
|
+
<Trans>Select facet values to assign to {productIds.length} products</Trans>
|
|
69
|
+
</DialogDescription>
|
|
70
|
+
</DialogHeader>
|
|
71
|
+
<div className="grid gap-4 py-4">
|
|
72
|
+
<div className="grid gap-2">
|
|
73
|
+
<label className="text-sm font-medium">
|
|
74
|
+
<Trans>Facet values</Trans>
|
|
75
|
+
</label>
|
|
76
|
+
<FacetValueSelector
|
|
77
|
+
onValueSelect={handleFacetValueSelect}
|
|
78
|
+
placeholder="Search facet values..."
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
{selectedFacetValueIds.length > 0 && (
|
|
82
|
+
<div className="text-sm text-muted-foreground">
|
|
83
|
+
<Trans>{selectedFacetValueIds.length} facet value(s) selected</Trans>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<DialogFooter>
|
|
88
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
89
|
+
<Trans>Cancel</Trans>
|
|
90
|
+
</Button>
|
|
91
|
+
<Button onClick={handleAssign} disabled={selectedFacetValueIds.length === 0 || isPending}>
|
|
92
|
+
<Trans>Update</Trans>
|
|
93
|
+
</Button>
|
|
94
|
+
</DialogFooter>
|
|
95
|
+
</DialogContent>
|
|
96
|
+
</Dialog>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { useMutation } from '@tanstack/react-query';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
|
|
5
|
+
import { ChannelCodeLabel } from '@/components/shared/channel-code-label.js';
|
|
6
|
+
import { Button } from '@/components/ui/button.js';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/components/ui/dialog.js';
|
|
15
|
+
import { Input } from '@/components/ui/input.js';
|
|
16
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select.js';
|
|
17
|
+
import { api } from '@/graphql/api.js';
|
|
18
|
+
import { ResultOf } from '@/graphql/graphql.js';
|
|
19
|
+
import { Trans, useLingui } from '@/lib/trans.js';
|
|
20
|
+
|
|
21
|
+
import { useChannel } from '@/hooks/use-channel.js';
|
|
22
|
+
import { assignProductsToChannelDocument } from '../products.graphql.js';
|
|
23
|
+
|
|
24
|
+
interface AssignToChannelDialogProps {
|
|
25
|
+
open: boolean;
|
|
26
|
+
onOpenChange: (open: boolean) => void;
|
|
27
|
+
productIds: string[];
|
|
28
|
+
onSuccess?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AssignToChannelDialog({
|
|
32
|
+
open,
|
|
33
|
+
onOpenChange,
|
|
34
|
+
productIds,
|
|
35
|
+
onSuccess,
|
|
36
|
+
}: AssignToChannelDialogProps) {
|
|
37
|
+
const { i18n } = useLingui();
|
|
38
|
+
const [selectedChannelId, setSelectedChannelId] = useState<string>('');
|
|
39
|
+
const [priceFactor, setPriceFactor] = useState<number>(1);
|
|
40
|
+
const { channels, selectedChannel } = useChannel();
|
|
41
|
+
|
|
42
|
+
// Filter out the currently selected channel from available options
|
|
43
|
+
const availableChannels = channels.filter(channel => channel.id !== selectedChannel?.id);
|
|
44
|
+
|
|
45
|
+
const { mutate, isPending } = useMutation({
|
|
46
|
+
mutationFn: api.mutate(assignProductsToChannelDocument),
|
|
47
|
+
onSuccess: (result: ResultOf<typeof assignProductsToChannelDocument>) => {
|
|
48
|
+
toast.success(i18n.t(`Successfully assigned ${productIds.length} products to channel`));
|
|
49
|
+
onSuccess?.();
|
|
50
|
+
onOpenChange(false);
|
|
51
|
+
},
|
|
52
|
+
onError: () => {
|
|
53
|
+
toast.error(`Failed to assign ${productIds.length} products to channel`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handleAssign = () => {
|
|
58
|
+
if (!selectedChannelId) {
|
|
59
|
+
toast.error('Please select a channel');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mutate({
|
|
64
|
+
input: {
|
|
65
|
+
productIds,
|
|
66
|
+
channelId: selectedChannelId,
|
|
67
|
+
priceFactor,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
74
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
75
|
+
<DialogHeader>
|
|
76
|
+
<DialogTitle>
|
|
77
|
+
<Trans>Assign products to channel</Trans>
|
|
78
|
+
</DialogTitle>
|
|
79
|
+
<DialogDescription>
|
|
80
|
+
<Trans>Select a channel to assign {productIds.length} products to</Trans>
|
|
81
|
+
</DialogDescription>
|
|
82
|
+
</DialogHeader>
|
|
83
|
+
<div className="grid gap-4 py-4">
|
|
84
|
+
<div className="grid gap-2">
|
|
85
|
+
<label className="text-sm font-medium">
|
|
86
|
+
<Trans>Channel</Trans>
|
|
87
|
+
</label>
|
|
88
|
+
<Select value={selectedChannelId} onValueChange={setSelectedChannelId}>
|
|
89
|
+
<SelectTrigger>
|
|
90
|
+
<SelectValue placeholder={i18n.t('Select a channel')} />
|
|
91
|
+
</SelectTrigger>
|
|
92
|
+
<SelectContent>
|
|
93
|
+
{availableChannels.map(channel => (
|
|
94
|
+
<SelectItem key={channel.id} value={channel.id}>
|
|
95
|
+
<ChannelCodeLabel code={channel.code} />
|
|
96
|
+
</SelectItem>
|
|
97
|
+
))}
|
|
98
|
+
</SelectContent>
|
|
99
|
+
</Select>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="grid gap-2">
|
|
102
|
+
<label className="text-sm font-medium">
|
|
103
|
+
<Trans>Price conversion factor</Trans>
|
|
104
|
+
</label>
|
|
105
|
+
<Input
|
|
106
|
+
type="number"
|
|
107
|
+
min="0"
|
|
108
|
+
max="99999"
|
|
109
|
+
step="0.01"
|
|
110
|
+
value={priceFactor}
|
|
111
|
+
onChange={e => setPriceFactor(parseFloat(e.target.value) || 1)}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<DialogFooter>
|
|
116
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
117
|
+
<Trans>Cancel</Trans>
|
|
118
|
+
</Button>
|
|
119
|
+
<Button onClick={handleAssign} disabled={!selectedChannelId || isPending}>
|
|
120
|
+
<Trans>Assign</Trans>
|
|
121
|
+
</Button>
|
|
122
|
+
</DialogFooter>
|
|
123
|
+
</DialogContent>
|
|
124
|
+
</Dialog>
|
|
125
|
+
);
|
|
126
|
+
}
|