@vendure/dashboard 3.2.2 → 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.
Files changed (92) hide show
  1. package/dist/plugin/utils/ast-utils.d.ts +10 -0
  2. package/dist/plugin/utils/ast-utils.js +96 -0
  3. package/dist/plugin/utils/ast-utils.spec.d.ts +1 -0
  4. package/dist/plugin/utils/ast-utils.spec.js +120 -0
  5. package/dist/plugin/{config-loader.d.ts → utils/config-loader.d.ts} +22 -8
  6. package/dist/plugin/utils/config-loader.js +325 -0
  7. package/dist/plugin/{schema-generator.d.ts → utils/schema-generator.d.ts} +5 -0
  8. package/dist/plugin/{schema-generator.js → utils/schema-generator.js} +6 -0
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -2
  10. package/dist/plugin/vite-plugin-admin-api-schema.js +2 -2
  11. package/dist/plugin/vite-plugin-config-loader.d.ts +2 -3
  12. package/dist/plugin/vite-plugin-config-loader.js +18 -9
  13. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  14. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  15. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  16. package/package.json +8 -6
  17. package/src/app/app-providers.tsx +8 -8
  18. package/src/app/main.tsx +1 -1
  19. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  20. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  21. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  22. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  23. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  24. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  25. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  26. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  27. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  29. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  30. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -1
  31. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  32. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  33. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  34. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  35. package/src/app/routes/_authenticated.tsx +12 -1
  36. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  37. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  38. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  39. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  40. package/src/lib/components/data-table/data-table-types.ts +1 -0
  41. package/src/lib/components/data-table/data-table-view-options.tsx +72 -23
  42. package/src/lib/components/data-table/data-table.tsx +23 -24
  43. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  44. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  45. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  46. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  47. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  48. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  49. package/src/lib/components/layout/nav-user.tsx +4 -4
  50. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  51. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  52. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  53. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  54. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  55. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  56. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  57. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  58. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  59. package/src/lib/components/shared/customer-selector.tsx +13 -14
  60. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  61. package/src/lib/components/shared/entity-assets.tsx +3 -3
  62. package/src/lib/components/shared/navigation-confirmation.tsx +39 -0
  63. package/src/lib/components/shared/paginated-list-data-table.tsx +9 -1
  64. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  65. package/src/lib/components/shared/vendure-image.tsx +1 -1
  66. package/src/lib/components/ui/calendar.tsx +508 -63
  67. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  68. package/src/lib/framework/document-introspection/get-document-structure.ts +70 -11
  69. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  70. package/src/lib/framework/layout-engine/page-layout.tsx +4 -0
  71. package/src/lib/framework/page/list-page.tsx +23 -4
  72. package/src/lib/framework/page/use-detail-page.ts +1 -0
  73. package/src/lib/graphql/fragments.tsx +8 -0
  74. package/src/lib/index.ts +5 -5
  75. package/src/lib/providers/auth.tsx +12 -9
  76. package/src/lib/providers/channel-provider.tsx +1 -0
  77. package/src/lib/providers/server-config.tsx +7 -1
  78. package/src/lib/providers/user-settings.tsx +24 -0
  79. package/vite/utils/ast-utils.spec.ts +128 -0
  80. package/vite/utils/ast-utils.ts +119 -0
  81. package/vite/utils/config-loader.ts +410 -0
  82. package/vite/{schema-generator.ts → utils/schema-generator.ts} +7 -1
  83. package/vite/{ui-config.ts → utils/ui-config.ts} +2 -2
  84. package/vite/vite-plugin-admin-api-schema.ts +2 -2
  85. package/vite/vite-plugin-config-loader.ts +25 -13
  86. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  87. package/vite/vite-plugin-gql-tada.ts +2 -2
  88. package/vite/vite-plugin-ui-config.ts +3 -2
  89. package/dist/plugin/config-loader.js +0 -141
  90. package/src/lib/components/shared/asset-preview.tsx +0 -345
  91. package/vite/config-loader.ts +0 -181
  92. /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 vendureConfig;
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 result = await loadVendureConfig({
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
- vendureConfig = result.vendureConfig;
21
- this.info(`Vendure config loaded (using export "${result.exportedSymbolName}")`);
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 (vendureConfig) {
33
- return Promise.resolve(vendureConfig);
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(vendureConfig);
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 vendureConfig;
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 (!vendureConfig) {
28
- vendureConfig = await configLoaderApi.getVendureConfig();
26
+ if (!loadVendureConfigResult) {
27
+ loadVendureConfigResult = await configLoaderApi.getVendureConfig();
29
28
  }
30
- const extensions = getPluginDashboardExtensions((_a = vendureConfig.plugins) !== null && _a !== void 0 ? _a : []);
31
- const extensionData = extensions.map(extension => {
32
- const providedPath = typeof extension === 'string' ? extension : extension.location;
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
- ${extensionData.map(extension => `await import('${extension.importPath}');`).join('\n')}
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
- vendureConfig = await configLoaderApi.getVendureConfig();
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.2",
4
+ "version": "3.2.4",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "scripts": {
16
16
  "dev": "vite",
17
+ "build:standalone": "vite build",
17
18
  "build": "tsc --project tsconfig.plugin.json",
18
19
  "watch": "tsc --project tsconfig.plugin.json --watch",
19
20
  "test": "vitest run",
@@ -45,6 +46,7 @@
45
46
  ],
46
47
  "dependencies": {
47
48
  "@dnd-kit/core": "^6.3.1",
49
+ "@dnd-kit/modifiers": "^9.0.0",
48
50
  "@dnd-kit/sortable": "^10.0.0",
49
51
  "@hookform/resolvers": "^4.1.3",
50
52
  "@lingui/babel-plugin-lingui-macro": "^5.2.0",
@@ -84,8 +86,8 @@
84
86
  "@types/react-dom": "^19.0.4",
85
87
  "@types/react-grid-layout": "^1.3.5",
86
88
  "@uidotdev/usehooks": "^2.4.1",
87
- "@vendure/common": "3.2.2",
88
- "@vendure/core": "3.2.2",
89
+ "@vendure/common": "3.2.4",
90
+ "@vendure/core": "3.2.4",
89
91
  "@vitejs/plugin-react": "^4.3.4",
90
92
  "awesome-graphql-client": "^2.1.0",
91
93
  "class-variance-authority": "^0.7.1",
@@ -99,7 +101,7 @@
99
101
  "motion": "^12.6.2",
100
102
  "next-themes": "^0.4.6",
101
103
  "react": "^19.0.0",
102
- "react-day-picker": "^8.10.1",
104
+ "react-day-picker": "^9.6.7",
103
105
  "react-dom": "^19.0.0",
104
106
  "react-dropzone": "^14.3.8",
105
107
  "react-grid-layout": "^1.5.1",
@@ -109,8 +111,8 @@
109
111
  "tailwind-merge": "^3.0.1",
110
112
  "tailwindcss": "^4.0.6",
111
113
  "tailwindcss-animate": "^1.0.7",
114
+ "tsconfig-paths": "^4.2.0",
112
115
  "tw-animate-css": "^1.2.4",
113
- "unplugin-swc": "^1.5.1",
114
116
  "vite": "^6.1.0",
115
117
  "zod": "^3.24.2"
116
118
  },
@@ -124,5 +126,5 @@
124
126
  "globals": "^15.14.0",
125
127
  "vite-plugin-dts": "^4.5.3"
126
128
  },
127
- "gitHead": "8729e7f8f028be556543c8e971e750228e516919"
129
+ "gitHead": "5434f3eca355aaf6ae3bf14f479f27d2387f3c3a"
128
130
  }
@@ -14,15 +14,15 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
14
14
  return (
15
15
  <I18nProvider>
16
16
  <QueryClientProvider client={queryClient}>
17
- <ServerConfigProvider>
18
- <UserSettingsProvider>
19
- <ThemeProvider defaultTheme="system">
20
- <AuthProvider>
17
+ <UserSettingsProvider>
18
+ <ThemeProvider defaultTheme="system">
19
+ <AuthProvider>
20
+ <ServerConfigProvider>
21
21
  <ChannelProvider>{children}</ChannelProvider>
22
- </AuthProvider>
23
- </ThemeProvider>
24
- </UserSettingsProvider>
25
- </ServerConfigProvider>
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
@@ -46,7 +46,7 @@ function InnerApp() {
46
46
  }
47
47
  setCustomFieldsMap(serverConfig.entityCustomFields);
48
48
  setHasSetCustomFieldsMap(true);
49
- }, [serverConfig?.entityCustomFields]);
49
+ }, [serverConfig?.entityCustomFields.length]);
50
50
 
51
51
  return (
52
52
  <>
@@ -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={false} />
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
+ }