@vendure/dashboard 3.2.3 → 3.3.0

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 (123) 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} +7 -1
  9. package/dist/plugin/{ui-config.js → utils/ui-config.js} +2 -3
  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-config.js +4 -6
  14. package/dist/plugin/vite-plugin-dashboard-metadata.js +12 -14
  15. package/dist/plugin/vite-plugin-gql-tada.js +2 -2
  16. package/dist/plugin/vite-plugin-ui-config.js +3 -2
  17. package/package.json +16 -11
  18. package/src/app/app-providers.tsx +9 -9
  19. package/src/app/main.tsx +1 -1
  20. package/src/app/routes/_authenticated/_assets/assets.graphql.ts +26 -0
  21. package/src/app/routes/_authenticated/_assets/assets.tsx +2 -2
  22. package/src/app/routes/_authenticated/_assets/assets_.$id.tsx +156 -0
  23. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +104 -0
  24. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +228 -0
  25. package/src/app/routes/_authenticated/_orders/components/money-gross-net.tsx +18 -0
  26. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -1
  27. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +38 -0
  28. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +53 -0
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +8 -49
  30. package/src/app/routes/_authenticated/_orders/components/shipping-method-selector.tsx +65 -0
  31. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +187 -2
  32. package/src/app/routes/_authenticated/_orders/orders.tsx +39 -18
  33. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +31 -9
  34. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +418 -0
  35. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +8 -2
  36. package/src/app/routes/_authenticated/_products/products.tsx +1 -1
  37. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -0
  38. package/src/app/routes/_authenticated/_system/job-queue.tsx +7 -8
  39. package/src/app/routes/_authenticated/_system/scheduled-tasks.tsx +241 -0
  40. package/src/app/routes/_authenticated.tsx +12 -1
  41. package/src/app/styles.css +15 -0
  42. package/src/lib/components/data-table/add-filter-menu.tsx +61 -0
  43. package/src/lib/components/data-table/data-table-column-header.tsx +0 -13
  44. package/src/lib/components/data-table/data-table-filter-badge.tsx +75 -0
  45. package/src/lib/components/data-table/data-table-filter-dialog.tsx +27 -28
  46. package/src/lib/components/data-table/data-table-types.ts +1 -0
  47. package/src/lib/components/data-table/data-table-view-options.tsx +73 -24
  48. package/src/lib/components/data-table/data-table.tsx +49 -44
  49. package/src/lib/components/data-table/filters/data-table-boolean-filter.tsx +57 -0
  50. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +93 -0
  51. package/src/lib/components/data-table/filters/data-table-id-filter.tsx +58 -0
  52. package/src/lib/components/data-table/filters/data-table-number-filter.tsx +119 -0
  53. package/src/lib/components/data-table/filters/data-table-string-filter.tsx +62 -0
  54. package/src/lib/components/data-table/human-readable-operator.tsx +65 -0
  55. package/src/lib/components/data-table/refresh-button.tsx +25 -0
  56. package/src/lib/components/layout/nav-user.tsx +20 -15
  57. package/src/lib/components/layout/prerelease-popup.tsx +1 -5
  58. package/src/lib/components/shared/alerts.tsx +19 -1
  59. package/src/lib/components/shared/asset/asset-focal-point-editor.tsx +93 -0
  60. package/src/lib/components/shared/{asset-gallery.tsx → asset/asset-gallery.tsx} +51 -20
  61. package/src/lib/components/shared/{asset-picker-dialog.tsx → asset/asset-picker-dialog.tsx} +1 -1
  62. package/src/lib/components/shared/{asset-preview-dialog.tsx → asset/asset-preview-dialog.tsx} +1 -7
  63. package/src/lib/components/shared/asset/asset-preview-selector.tsx +34 -0
  64. package/src/lib/components/shared/asset/asset-preview.tsx +128 -0
  65. package/src/lib/components/shared/asset/asset-properties.tsx +46 -0
  66. package/src/lib/components/shared/{focal-point-control.tsx → asset/focal-point-control.tsx} +1 -1
  67. package/src/lib/components/shared/custom-fields-form.tsx +4 -3
  68. package/src/lib/components/shared/customer-selector.tsx +13 -14
  69. package/src/lib/components/shared/detail-page-button.tsx +2 -2
  70. package/src/lib/components/shared/entity-assets.tsx +3 -3
  71. package/src/lib/components/shared/error-page.tsx +2 -2
  72. package/src/lib/components/shared/navigation-confirmation.tsx +49 -0
  73. package/src/lib/components/shared/paginated-list-data-table.tsx +10 -1
  74. package/src/lib/components/shared/product-variant-selector.tsx +111 -0
  75. package/src/lib/components/shared/vendure-image.tsx +1 -1
  76. package/src/lib/components/ui/calendar.tsx +508 -63
  77. package/src/lib/framework/alert/alert-extensions.tsx +31 -0
  78. package/src/lib/framework/alert/alert-item.tsx +47 -0
  79. package/src/lib/framework/alert/alerts-indicator.tsx +23 -0
  80. package/src/lib/framework/alert/types.ts +13 -0
  81. package/src/lib/framework/dashboard-widget/base-widget.tsx +1 -0
  82. package/src/lib/framework/defaults.ts +34 -0
  83. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +113 -3
  84. package/src/lib/framework/document-introspection/get-document-structure.ts +71 -13
  85. package/src/lib/framework/extension-api/define-dashboard-extension.ts +15 -5
  86. package/src/lib/framework/extension-api/extension-api-types.ts +81 -12
  87. package/src/lib/framework/form-engine/use-generated-form.tsx +8 -7
  88. package/src/lib/framework/layout-engine/layout-extensions.ts +3 -3
  89. package/src/lib/framework/layout-engine/page-layout.tsx +196 -35
  90. package/src/lib/framework/layout-engine/page-provider.tsx +10 -0
  91. package/src/lib/framework/page/detail-page.tsx +62 -9
  92. package/src/lib/framework/page/list-page.tsx +42 -4
  93. package/src/lib/framework/page/page-api.ts +1 -1
  94. package/src/lib/framework/page/use-detail-page.ts +82 -0
  95. package/src/lib/framework/registry/registry-types.ts +6 -2
  96. package/src/lib/graphql/fragments.tsx +8 -0
  97. package/src/lib/graphql/graphql-env.d.ts +25 -9
  98. package/src/lib/hooks/use-auth.tsx +13 -1
  99. package/src/lib/hooks/use-channel.ts +13 -0
  100. package/src/lib/hooks/use-local-format.ts +28 -1
  101. package/src/lib/hooks/use-page.tsx +2 -3
  102. package/src/lib/hooks/use-permissions.ts +13 -0
  103. package/src/lib/index.ts +7 -8
  104. package/src/lib/providers/auth.tsx +22 -9
  105. package/src/lib/providers/channel-provider.tsx +9 -1
  106. package/src/lib/providers/server-config.tsx +7 -1
  107. package/src/lib/providers/user-settings.tsx +24 -0
  108. package/vite/utils/ast-utils.spec.ts +128 -0
  109. package/vite/utils/ast-utils.ts +119 -0
  110. package/vite/utils/config-loader.ts +410 -0
  111. package/vite/{schema-generator.ts → utils/schema-generator.ts} +11 -6
  112. package/vite/{ui-config.ts → utils/ui-config.ts} +7 -3
  113. package/vite/vite-plugin-admin-api-schema.ts +2 -12
  114. package/vite/vite-plugin-config-loader.ts +25 -13
  115. package/vite/vite-plugin-config.ts +1 -0
  116. package/vite/vite-plugin-dashboard-metadata.ts +19 -15
  117. package/vite/vite-plugin-gql-tada.ts +2 -2
  118. package/vite/vite-plugin-ui-config.ts +3 -2
  119. package/dist/plugin/config-loader.js +0 -141
  120. package/src/lib/components/shared/asset-preview.tsx +0 -345
  121. package/src/lib/components/ui/avatar.tsx +0 -38
  122. package/vite/config-loader.ts +0 -181
  123. /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
  }
@@ -3,17 +3,15 @@ export function viteConfigPlugin({ packageRoot }) {
3
3
  return {
4
4
  name: 'vendure:vite-config-plugin',
5
5
  config: (config) => {
6
- var _a, _b;
6
+ var _a, _b, _c, _d;
7
7
  config.root = packageRoot;
8
8
  config.resolve = {
9
- alias: {
10
- '@': path.resolve(packageRoot, './src/lib'),
11
- },
9
+ alias: Object.assign(Object.assign({}, ((_b = (_a = config.resolve) === null || _a === void 0 ? void 0 : _a.alias) !== null && _b !== void 0 ? _b : {})), { '@': path.resolve(packageRoot, './src/lib') }),
12
10
  };
13
11
  // This is required to prevent Vite from pre-bundling the
14
12
  // dashboard source when it resides in node_modules.
15
13
  config.optimizeDeps = Object.assign(Object.assign({}, config.optimizeDeps), { exclude: [
16
- ...(((_a = config.optimizeDeps) === null || _a === void 0 ? void 0 : _a.exclude) || []),
14
+ ...(((_c = config.optimizeDeps) === null || _c === void 0 ? void 0 : _c.exclude) || []),
17
15
  '@vendure/dashboard',
18
16
  '@/providers',
19
17
  '@/framework',
@@ -28,7 +26,7 @@ export function viteConfigPlugin({ packageRoot }) {
28
26
  // on lodash which is a CJS packages and _does_ require
29
27
  // pre-bundling.
30
28
  include: [
31
- ...(((_b = config.optimizeDeps) === null || _b === void 0 ? void 0 : _b.include) || []),
29
+ ...(((_d = config.optimizeDeps) === null || _d === void 0 ? void 0 : _d.include) || []),
32
30
  '@/components > recharts',
33
31
  '@/components > react-dropzone',
34
32
  ] });
@@ -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.3",
4
+ "version": "3.3.0",
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",
@@ -70,7 +71,7 @@
70
71
  "@radix-ui/react-switch": "^1.1.3",
71
72
  "@radix-ui/react-tabs": "^1.1.3",
72
73
  "@radix-ui/react-tooltip": "^1.1.8",
73
- "@tailwindcss/vite": "^4.0.7",
74
+ "@tailwindcss/vite": "^4.1.5",
74
75
  "@tanstack/eslint-plugin-query": "^5.66.1",
75
76
  "@tanstack/react-query": "^5.66.7",
76
77
  "@tanstack/react-query-devtools": "^5.68.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.3",
89
- "@vendure/core": "3.2.3",
89
+ "@vendure/common": "3.3.0",
90
+ "@vendure/core": "3.3.0",
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,19 +101,19 @@
100
101
  "motion": "^12.6.2",
101
102
  "next-themes": "^0.4.6",
102
103
  "react": "^19.0.0",
103
- "react-day-picker": "^8.10.1",
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",
107
108
  "react-hook-form": "^7.54.2",
108
109
  "recharts": "^2.15.1",
109
110
  "sonner": "^2.0.1",
110
- "tailwind-merge": "^3.0.1",
111
- "tailwindcss": "^4.0.6",
111
+ "tailwind-merge": "^3.2.0",
112
+ "tailwindcss": "^4.1.5",
112
113
  "tailwindcss-animate": "^1.0.7",
113
- "tw-animate-css": "^1.2.4",
114
- "unplugin-swc": "^1.5.1",
115
- "vite": "^6.1.0",
114
+ "tsconfig-paths": "^4.2.0",
115
+ "tw-animate-css": "^1.2.9",
116
+ "vite": "^6.3.5",
116
117
  "zod": "^3.24.2"
117
118
  },
118
119
  "devDependencies": {
@@ -125,5 +126,9 @@
125
126
  "globals": "^15.14.0",
126
127
  "vite-plugin-dts": "^4.5.3"
127
128
  },
128
- "gitHead": "5383d7640d04ee772a9c5dbb5a0389c8b8951f66"
129
+ "optionalDependencies": {
130
+ "lightningcss-linux-arm64-musl": "^1.29.3",
131
+ "lightningcss-linux-x64-musl": "^1.29.1"
132
+ },
133
+ "gitHead": "a4c56359ec831b0c54dd01eab14fb75455c43ab7"
129
134
  }
@@ -14,16 +14,16 @@ 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>
26
- <ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
22
+ </ServerConfigProvider>
23
+ </AuthProvider>
24
+ </ThemeProvider>
25
+ </UserSettingsProvider>
26
+ {/*<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />*/}
27
27
  </QueryClientProvider>
28
28
  </I18nProvider>
29
29
  );
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
+ }