@vendure/dashboard 3.2.3 → 3.2.4
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/utils/ast-utils.d.ts +10 -0
- package/dist/plugin/utils/ast-utils.js +96 -0
- package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
- package/dist/plugin/utils/ast-utils.spec.js +120 -0
- package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
- package/dist/plugin/utils/config-loader.js +325 -0
- package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
- package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +6 -0
- package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
- package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
- package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
- package/dist/plugin/vite-plugin-config-loader.js +18 -9
- package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
- package/dist/plugin/vite-plugin-gql-tada.js +2 -2
- package/dist/plugin/vite-plugin-ui-config.js +3 -2
- package/package.json +7 -6
- package/src/app/app-providers.tsx +8 -8
- package/src/app/main.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
- package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
- package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
- package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
- package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -1
- package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
- package/src/app/routes/_authenticated/_products/products.tsx +1 -1
- package/src/app/routes/_authenticated.tsx +12 -1
- package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
- package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
- package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
- package/src/lib/components/data-table/data-table-types.ts +1 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +72 -23
- package/src/lib/components/data-table/data-table.tsx +23 -24
- package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
- package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
- package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
- package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
- package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
- package/src/lib/components/layout/nav-user.tsx +4 -4
- package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
- package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
- package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
- package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
- package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
- package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
- package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
- package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
- package/src/lib/components/shared/custom-fields-form.tsx +4 -3
- package/src/lib/components/shared/customer-selector.tsx +13 -14
- package/src/lib/components/shared/detail-page-button.tsx +2 -2
- package/src/lib/components/shared/entity-assets.tsx +3 -3
- package/src/lib/components/shared/navigation-confirmation.tsx +39 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +9 -1
- package/src/lib/components/shared/product-variant-selector.tsx +111 -0
- package/src/lib/components/shared/vendure-image.tsx +1 -1
- package/src/lib/components/ui/calendar.tsx +508 -63
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
- package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
- package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
- package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
- package/src/lib/framework/page/list-page.tsx +23 -4
- package/src/lib/framework/page/use-detail-page.ts +1 -0
- package/src/lib/graphql/fragments.tsx +8 -0
- package/src/lib/index.ts +5 -5
- package/src/lib/providers/auth.tsx +12 -9
- package/src/lib/providers/channel-provider.tsx +1 -0
- package/src/lib/providers/server-config.tsx +7 -1
- package/src/lib/providers/user-settings.tsx +24 -0
- package/vite/utils/ast-utils.spec.ts +128 -0
- package/vite/utils/ast-utils.ts +119 -0
- package/vite/utils/config-loader.ts +410 -0
- package/vite/{schema-generator.ts → utils/schema-generator.ts} +7 -1
- package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
- package/vite/vite-plugin-admin-api-schema.ts +2 -2
- package/vite/vite-plugin-config-loader.ts +25 -13
- package/vite/vite-plugin-dashboard-metadata.ts +19 -15
- package/vite/vite-plugin-gql-tada.ts +2 -2
- package/vite/vite-plugin-ui-config.ts +3 -2
- package/dist/plugin/config-loader.js +0 -141
- package/src/lib/components/shared/asset-preview.tsx +0 -345
- package/vite/config-loader.ts +0 -181
- /package/dist/plugin/{ui-config.d.ts → utils/ui-config.d.ts} +0 -0
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
import { loadVendureConfig } from './config-loader.js';
|
|
1
|
+
import { loadVendureConfig } from './utils/config-loader.js';
|
|
2
2
|
export const configLoaderName = 'vendure:config-loader';
|
|
3
3
|
/**
|
|
4
4
|
* This Vite plugin loads the VendureConfig from the specified file path, and
|
|
5
5
|
* makes it available to other plugins via the `ConfigLoaderApi`.
|
|
6
6
|
*/
|
|
7
7
|
export function configLoaderPlugin(options) {
|
|
8
|
-
let
|
|
8
|
+
let result;
|
|
9
9
|
const onConfigLoaded = [];
|
|
10
10
|
return {
|
|
11
11
|
name: configLoaderName,
|
|
12
12
|
async buildStart() {
|
|
13
|
-
this.info(`Loading Vendure config...`);
|
|
13
|
+
this.info(`Loading Vendure config. This can take a short while depending on the size of your project...`);
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
result = await loadVendureConfig({
|
|
16
17
|
tempDir: options.tempDir,
|
|
17
18
|
vendureConfigPath: options.vendureConfigPath,
|
|
18
19
|
vendureConfigExport: options.vendureConfigExport,
|
|
20
|
+
logger: {
|
|
21
|
+
info: (message) => this.info(message),
|
|
22
|
+
warn: (message) => this.warn(message),
|
|
23
|
+
debug: (message) => this.debug(message),
|
|
24
|
+
},
|
|
19
25
|
});
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
const endTime = Date.now();
|
|
27
|
+
const duration = endTime - startTime;
|
|
28
|
+
const pluginNames = result.pluginInfo.map(p => p.name).join(', ');
|
|
29
|
+
this.info(`Found ${result.pluginInfo.length} plugins: ${pluginNames}`);
|
|
30
|
+
this.info(`Vendure config loaded (using export "${result.exportedSymbolName}") in ${duration}ms`);
|
|
22
31
|
}
|
|
23
32
|
catch (e) {
|
|
24
33
|
if (e instanceof Error) {
|
|
@@ -29,13 +38,13 @@ export function configLoaderPlugin(options) {
|
|
|
29
38
|
},
|
|
30
39
|
api: {
|
|
31
40
|
getVendureConfig() {
|
|
32
|
-
if (
|
|
33
|
-
return Promise.resolve(
|
|
41
|
+
if (result) {
|
|
42
|
+
return Promise.resolve(result);
|
|
34
43
|
}
|
|
35
44
|
else {
|
|
36
45
|
return new Promise(resolve => {
|
|
37
46
|
onConfigLoaded.push(() => {
|
|
38
|
-
resolve(
|
|
47
|
+
resolve(result);
|
|
39
48
|
});
|
|
40
49
|
});
|
|
41
50
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getPluginDashboardExtensions } from '@vendure/core';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
|
|
4
3
|
const virtualModuleId = 'virtual:dashboard-extensions';
|
|
@@ -10,7 +9,7 @@ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
|
10
9
|
*/
|
|
11
10
|
export function dashboardMetadataPlugin(options) {
|
|
12
11
|
let configLoaderApi;
|
|
13
|
-
let
|
|
12
|
+
let loadVendureConfigResult;
|
|
14
13
|
return {
|
|
15
14
|
name: 'vendure:dashboard-extensions-metadata',
|
|
16
15
|
configResolved({ plugins }) {
|
|
@@ -24,21 +23,20 @@ export function dashboardMetadataPlugin(options) {
|
|
|
24
23
|
async load(id) {
|
|
25
24
|
var _a;
|
|
26
25
|
if (id === resolvedVirtualModuleId) {
|
|
27
|
-
if (!
|
|
28
|
-
|
|
26
|
+
if (!loadVendureConfigResult) {
|
|
27
|
+
loadVendureConfigResult = await configLoaderApi.getVendureConfig();
|
|
29
28
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const jsPath = normalizeImportPath(options.rootDir, providedPath);
|
|
34
|
-
return { importPath: `./${jsPath}` };
|
|
35
|
-
});
|
|
36
|
-
this.info(`Found ${extensionData.length} Dashboard extensions`);
|
|
29
|
+
const { pluginInfo } = loadVendureConfigResult;
|
|
30
|
+
const pluginsWithExtensions = (_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.map(({ dashboardEntryPath, pluginPath }) => dashboardEntryPath && path.join(pluginPath, dashboardEntryPath)).filter(x => x != null)) !== null && _a !== void 0 ? _a : [];
|
|
31
|
+
this.info(`Found ${pluginsWithExtensions.length} Dashboard extensions`);
|
|
37
32
|
return `
|
|
38
33
|
export async function runDashboardExtensions() {
|
|
39
|
-
${
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
${pluginsWithExtensions
|
|
35
|
+
.map(extension => {
|
|
36
|
+
return `await import('${extension}');`;
|
|
37
|
+
})
|
|
38
|
+
.join('\n')}
|
|
39
|
+
}`;
|
|
42
40
|
}
|
|
43
41
|
},
|
|
44
42
|
};
|
|
@@ -2,7 +2,7 @@ import { generateOutput } from '@gql.tada/cli-utils';
|
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import { printSchema } from 'graphql';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import { generateSchema } from './schema-generator.js';
|
|
5
|
+
import { generateSchema } from './utils/schema-generator.js';
|
|
6
6
|
import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
|
|
7
7
|
export function gqlTadaPlugin(options) {
|
|
8
8
|
let configLoaderApi;
|
|
@@ -12,7 +12,7 @@ export function gqlTadaPlugin(options) {
|
|
|
12
12
|
configLoaderApi = getConfigLoaderApi(plugins);
|
|
13
13
|
},
|
|
14
14
|
async buildStart() {
|
|
15
|
-
const vendureConfig = await configLoaderApi.getVendureConfig();
|
|
15
|
+
const { vendureConfig } = await configLoaderApi.getVendureConfig();
|
|
16
16
|
const safeSchema = await generateSchema({ vendureConfig });
|
|
17
17
|
const tsConfigContent = {
|
|
18
18
|
compilerOptions: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { getAdminUiConfig } from './ui-config.js';
|
|
2
|
+
import { getAdminUiConfig } from './utils/ui-config.js';
|
|
3
3
|
import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
|
|
4
4
|
const virtualModuleId = 'virtual:vendure-ui-config';
|
|
5
5
|
const resolvedVirtualModuleId = `\0${virtualModuleId}`;
|
|
@@ -24,7 +24,8 @@ export function uiConfigPlugin({ adminUiConfig }) {
|
|
|
24
24
|
async load(id) {
|
|
25
25
|
if (id === resolvedVirtualModuleId) {
|
|
26
26
|
if (!vendureConfig) {
|
|
27
|
-
|
|
27
|
+
const result = await configLoaderApi.getVendureConfig();
|
|
28
|
+
vendureConfig = result.vendureConfig;
|
|
28
29
|
}
|
|
29
30
|
const config = getAdminUiConfig(vendureConfig, adminUiConfig);
|
|
30
31
|
return `
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.2.
|
|
4
|
+
"version": "3.2.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
],
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@dnd-kit/core": "^6.3.1",
|
|
49
|
+
"@dnd-kit/modifiers": "^9.0.0",
|
|
49
50
|
"@dnd-kit/sortable": "^10.0.0",
|
|
50
51
|
"@hookform/resolvers": "^4.1.3",
|
|
51
52
|
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
|
|
@@ -85,8 +86,8 @@
|
|
|
85
86
|
"@types/react-dom": "^19.0.4",
|
|
86
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
87
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
88
|
-
"@vendure/common": "3.2.
|
|
89
|
-
"@vendure/core": "3.2.
|
|
89
|
+
"@vendure/common": "3.2.4",
|
|
90
|
+
"@vendure/core": "3.2.4",
|
|
90
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
91
92
|
"awesome-graphql-client": "^2.1.0",
|
|
92
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -100,7 +101,7 @@
|
|
|
100
101
|
"motion": "^12.6.2",
|
|
101
102
|
"next-themes": "^0.4.6",
|
|
102
103
|
"react": "^19.0.0",
|
|
103
|
-
"react-day-picker": "^
|
|
104
|
+
"react-day-picker": "^9.6.7",
|
|
104
105
|
"react-dom": "^19.0.0",
|
|
105
106
|
"react-dropzone": "^14.3.8",
|
|
106
107
|
"react-grid-layout": "^1.5.1",
|
|
@@ -110,8 +111,8 @@
|
|
|
110
111
|
"tailwind-merge": "^3.0.1",
|
|
111
112
|
"tailwindcss": "^4.0.6",
|
|
112
113
|
"tailwindcss-animate": "^1.0.7",
|
|
114
|
+
"tsconfig-paths": "^4.2.0",
|
|
113
115
|
"tw-animate-css": "^1.2.4",
|
|
114
|
-
"unplugin-swc": "^1.5.1",
|
|
115
116
|
"vite": "^6.1.0",
|
|
116
117
|
"zod": "^3.24.2"
|
|
117
118
|
},
|
|
@@ -125,5 +126,5 @@
|
|
|
125
126
|
"globals": "^15.14.0",
|
|
126
127
|
"vite-plugin-dts": "^4.5.3"
|
|
127
128
|
},
|
|
128
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "5434f3eca355aaf6ae3bf14f479f27d2387f3c3a"
|
|
129
130
|
}
|
|
@@ -14,15 +14,15 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
|
14
14
|
return (
|
|
15
15
|
<I18nProvider>
|
|
16
16
|
<QueryClientProvider client={queryClient}>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
17
|
+
<UserSettingsProvider>
|
|
18
|
+
<ThemeProvider defaultTheme="system">
|
|
19
|
+
<AuthProvider>
|
|
20
|
+
<ServerConfigProvider>
|
|
21
21
|
<ChannelProvider>{children}</ChannelProvider>
|
|
22
|
-
</
|
|
23
|
-
</
|
|
24
|
-
</
|
|
25
|
-
</
|
|
22
|
+
</ServerConfigProvider>
|
|
23
|
+
</AuthProvider>
|
|
24
|
+
</ThemeProvider>
|
|
25
|
+
</UserSettingsProvider>
|
|
26
26
|
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
|
|
27
27
|
</QueryClientProvider>
|
|
28
28
|
</I18nProvider>
|
package/src/app/main.tsx
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { assetFragment } from '@/graphql/fragments.js';
|
|
2
|
+
import { graphql } from '@/graphql/graphql.js';
|
|
3
|
+
|
|
4
|
+
export const assetDetailDocument = graphql(
|
|
5
|
+
`
|
|
6
|
+
query AssetDetail($id: ID!) {
|
|
7
|
+
asset(id: $id) {
|
|
8
|
+
...Asset
|
|
9
|
+
tags {
|
|
10
|
+
id
|
|
11
|
+
value
|
|
12
|
+
}
|
|
13
|
+
customFields
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
`,
|
|
17
|
+
[assetFragment],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const assetUpdateDocument = graphql(`
|
|
21
|
+
mutation AssetUpdate($input: UpdateAssetInput!) {
|
|
22
|
+
updateAsset(input: $input) {
|
|
23
|
+
id
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssetGallery } from '@/components/shared/asset-gallery.js';
|
|
1
|
+
import { AssetGallery } from '@/components/shared/asset/asset-gallery.js';
|
|
2
2
|
import { Page, PageTitle, PageActionBar } from '@/framework/layout-engine/page-layout.js';
|
|
3
3
|
import { Trans } from '@/lib/trans.js';
|
|
4
4
|
import { createFileRoute } from '@tanstack/react-router';
|
|
@@ -13,7 +13,7 @@ function RouteComponent() {
|
|
|
13
13
|
<PageTitle>
|
|
14
14
|
<Trans>Assets</Trans>
|
|
15
15
|
</PageTitle>
|
|
16
|
-
<AssetGallery selectable={
|
|
16
|
+
<AssetGallery selectable={true} multiSelect='manual' />
|
|
17
17
|
</Page>
|
|
18
18
|
);
|
|
19
19
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { AssetPreview } from '@/components/shared/asset/asset-preview.js'
|
|
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';
|
|
15
|
+
import { AssetPreviewSelector } from '@/components/shared/asset/asset-preview-selector.js';
|
|
16
|
+
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
|
+
import { Point } from '@/components/shared/asset/focal-point-control.js';
|
|
20
|
+
import { Label } from '@/components/ui/label.js';
|
|
21
|
+
export const Route = createFileRoute('/_authenticated/_assets/assets_/$id')({
|
|
22
|
+
component: AssetDetailPage,
|
|
23
|
+
loader: detailPageRouteLoader({
|
|
24
|
+
queryDocument: assetDetailDocument,
|
|
25
|
+
breadcrumb(isNew, entity) {
|
|
26
|
+
return [
|
|
27
|
+
{ path: '/assets', label: 'Assets' },
|
|
28
|
+
isNew ? <Trans>New asset</Trans> : entity?.name ?? '',
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function AssetDetailPage() {
|
|
36
|
+
const params = Route.useParams();
|
|
37
|
+
const { i18n } = useLingui();
|
|
38
|
+
|
|
39
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
40
|
+
const [size, setSize] = useState<PreviewPreset>('medium');
|
|
41
|
+
const [width, setWidth] = useState(0);
|
|
42
|
+
const [height, setHeight] = useState(0);
|
|
43
|
+
const [focalPoint, setFocalPoint] = useState<Point | undefined>(undefined);
|
|
44
|
+
const [settingFocalPoint, setSettingFocalPoint] = useState(false);
|
|
45
|
+
const { form, submitHandler, entity, isPending } = useDetailPage({
|
|
46
|
+
queryDocument: assetDetailDocument,
|
|
47
|
+
updateDocument: assetUpdateDocument,
|
|
48
|
+
setValuesForUpdate: entity => {
|
|
49
|
+
return {
|
|
50
|
+
id: entity.id,
|
|
51
|
+
focalPoint: entity.focalPoint,
|
|
52
|
+
name: entity.name,
|
|
53
|
+
tags: entity.tags?.map(tag => tag.value) ?? [],
|
|
54
|
+
customFields: entity.customFields,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
params: { id: params.id },
|
|
58
|
+
onSuccess: async () => {
|
|
59
|
+
toast(i18n.t('Successfully updated asset'));
|
|
60
|
+
form.reset(form.getValues());
|
|
61
|
+
},
|
|
62
|
+
onError: err => {
|
|
63
|
+
toast(i18n.t('Failed to update asset'), {
|
|
64
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const updateDimensions = () => {
|
|
70
|
+
if (!imageRef.current) return;
|
|
71
|
+
const img = imageRef.current;
|
|
72
|
+
const imgWidth = img.naturalWidth;
|
|
73
|
+
const imgHeight = img.naturalHeight;
|
|
74
|
+
setWidth(imgWidth);
|
|
75
|
+
setHeight(imgHeight);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (!entity) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<Page pageId="asset-detail" form={form} submitHandler={submitHandler}>
|
|
83
|
+
<PageTitle>
|
|
84
|
+
<Trans>Edit asset</Trans>
|
|
85
|
+
</PageTitle>
|
|
86
|
+
<PageActionBar>
|
|
87
|
+
<PageActionBarRight>
|
|
88
|
+
<PermissionGuard requires={['UpdateChannel']}>
|
|
89
|
+
<Button
|
|
90
|
+
type="submit"
|
|
91
|
+
disabled={!form.formState.isDirty || isPending}
|
|
92
|
+
>
|
|
93
|
+
<Trans>Update</Trans>
|
|
94
|
+
</Button>
|
|
95
|
+
</PermissionGuard>
|
|
96
|
+
</PageActionBarRight>
|
|
97
|
+
</PageActionBar>
|
|
98
|
+
<PageLayout>
|
|
99
|
+
<PageBlock column="main" blockId="asset-preview">
|
|
100
|
+
<div className="relative flex items-center justify-center bg-muted/30 rounded-lg min-h-[300px] overflow-auto">
|
|
101
|
+
<AssetFocalPointEditor
|
|
102
|
+
width={width}
|
|
103
|
+
height={height}
|
|
104
|
+
settingFocalPoint={settingFocalPoint}
|
|
105
|
+
focalPoint={form.getValues().focalPoint ?? { x: 0.5, y: 0.5 }}
|
|
106
|
+
onFocalPointChange={(point) => {
|
|
107
|
+
form.setValue('focalPoint.x', point.x, { shouldDirty: true });
|
|
108
|
+
form.setValue('focalPoint.y', point.y, { shouldDirty: true });
|
|
109
|
+
setSettingFocalPoint(false);
|
|
110
|
+
}}
|
|
111
|
+
onCancel={() => {
|
|
112
|
+
setSettingFocalPoint(false);
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<VendureImage
|
|
116
|
+
ref={imageRef}
|
|
117
|
+
asset={entity}
|
|
118
|
+
preset={size || undefined}
|
|
119
|
+
mode="resize"
|
|
120
|
+
useFocalPoint={true}
|
|
121
|
+
onLoad={updateDimensions}
|
|
122
|
+
className="max-w-full max-h-full object-contain"
|
|
123
|
+
/>
|
|
124
|
+
</AssetFocalPointEditor>
|
|
125
|
+
</div>
|
|
126
|
+
</PageBlock>
|
|
127
|
+
<CustomFieldsPageBlock
|
|
128
|
+
column="main"
|
|
129
|
+
entityType={'Asset'}
|
|
130
|
+
control={form.control}
|
|
131
|
+
/>
|
|
132
|
+
<PageBlock column="side" blockId="asset-properties">
|
|
133
|
+
<AssetProperties asset={entity} />
|
|
134
|
+
</PageBlock>
|
|
135
|
+
<PageBlock column="side" blockId="asset-size">
|
|
136
|
+
<div className="flex flex-col gap-2">
|
|
137
|
+
<AssetPreviewSelector size={size} setSize={setSize} width={width} height={height} />
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
<Button type='button' variant="outline" size="icon" onClick={() => setSettingFocalPoint(true)}>
|
|
140
|
+
<FocusIcon className="h-4 w-4" />
|
|
141
|
+
</Button>
|
|
142
|
+
<div className="text-sm text-muted-foreground">
|
|
143
|
+
<Label><Trans>Focal Point</Trans></Label>
|
|
144
|
+
<div className="text-sm text-muted-foreground">
|
|
145
|
+
{form.getValues().focalPoint?.x && form.getValues().focalPoint?.y
|
|
146
|
+
? `${form.getValues().focalPoint?.x.toFixed(2)}, ${form.getValues().focalPoint?.y.toFixed(2)}`
|
|
147
|
+
: <Trans>Not set</Trans>}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</PageBlock>
|
|
153
|
+
</PageLayout>
|
|
154
|
+
</Page>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button.js';
|
|
2
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover.js';
|
|
3
|
+
import { api } from '@/graphql/api.js';
|
|
4
|
+
import { graphql, ResultOf } from '@/graphql/graphql.js';
|
|
5
|
+
import { useQuery } from '@tanstack/react-query';
|
|
6
|
+
import { Trans } from '@/lib/trans.js';
|
|
7
|
+
import { useLingui } from '@/lib/trans.js';
|
|
8
|
+
import { addressFragment } from '../../_customers/customers.graphql.js';
|
|
9
|
+
import { Card } from '@/components/ui/card.js';
|
|
10
|
+
import { cn } from '@/lib/utils.js';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Plus } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
const getCustomerAddressesDocument = graphql(
|
|
15
|
+
`
|
|
16
|
+
query GetCustomerAddresses($customerId: ID!) {
|
|
17
|
+
customer(id: $customerId) {
|
|
18
|
+
id
|
|
19
|
+
addresses {
|
|
20
|
+
...Address
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
[addressFragment],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
type CustomerAddressesQuery = ResultOf<typeof getCustomerAddressesDocument>;
|
|
29
|
+
|
|
30
|
+
interface CustomerAddressSelectorProps {
|
|
31
|
+
customerId: string | undefined;
|
|
32
|
+
onSelect: (address: ResultOf<typeof addressFragment>) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function CustomerAddressSelector({ customerId, onSelect }: CustomerAddressSelectorProps) {
|
|
36
|
+
const { i18n } = useLingui();
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
|
|
39
|
+
const { data, isLoading } = useQuery<CustomerAddressesQuery>({
|
|
40
|
+
queryKey: ['customerAddresses', customerId],
|
|
41
|
+
queryFn: () => api.query(getCustomerAddressesDocument, { customerId: customerId ?? '' }),
|
|
42
|
+
enabled: !!customerId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const addresses: ResultOf<typeof addressFragment>[] = data?.customer?.addresses || [];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
49
|
+
<PopoverTrigger asChild>
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
<Button variant="outline" size="sm" type="button" className="" disabled={!customerId}>
|
|
52
|
+
<Plus className="h-4 w-4" />
|
|
53
|
+
<Trans>Select address</Trans>
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
</PopoverTrigger>
|
|
57
|
+
<PopoverContent className="w-[400px] p-0" align="start">
|
|
58
|
+
<div className="p-4">
|
|
59
|
+
<h4 className="mb-4">
|
|
60
|
+
<Trans>Select an address</Trans>
|
|
61
|
+
</h4>
|
|
62
|
+
<div className="space-y-2">
|
|
63
|
+
{isLoading ? (
|
|
64
|
+
<div className="text-sm text-muted-foreground">
|
|
65
|
+
<Trans>Loading addresses...</Trans>
|
|
66
|
+
</div>
|
|
67
|
+
) : addresses.length === 0 ? (
|
|
68
|
+
<div className="text-sm text-muted-foreground">
|
|
69
|
+
<Trans>No addresses found</Trans>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
addresses.map(address => (
|
|
73
|
+
<Card
|
|
74
|
+
key={address.id}
|
|
75
|
+
className={cn(
|
|
76
|
+
'p-4 cursor-pointer hover:bg-accent transition-colors',
|
|
77
|
+
)}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
onSelect(address);
|
|
80
|
+
setOpen(false);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex flex-col gap-1 text-sm">
|
|
84
|
+
<div className="font-semibold">{address.fullName}</div>
|
|
85
|
+
{address.company && <div>{address.company}</div>}
|
|
86
|
+
<div>{address.streetLine1}</div>
|
|
87
|
+
{address.streetLine2 && <div>{address.streetLine2}</div>}
|
|
88
|
+
<div>
|
|
89
|
+
{address.city}
|
|
90
|
+
{address.province && `, ${address.province}`}
|
|
91
|
+
</div>
|
|
92
|
+
<div>{address.postalCode}</div>
|
|
93
|
+
<div>{address.country.name}</div>
|
|
94
|
+
{address.phoneNumber && <div>{address.phoneNumber}</div>}
|
|
95
|
+
</div>
|
|
96
|
+
</Card>
|
|
97
|
+
))
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</PopoverContent>
|
|
102
|
+
</Popover>
|
|
103
|
+
);
|
|
104
|
+
}
|