@vendure/dashboard 3.5.1-master-202511120232 → 3.5.1-master-202511130232

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.
@@ -9,6 +9,7 @@ export interface CompilerOptions {
9
9
  pathAdapter?: PathAdapter;
10
10
  logger?: Logger;
11
11
  pluginPackageScanner?: PackageScannerConfig;
12
+ module?: 'commonjs' | 'esm';
12
13
  }
13
14
  export interface CompileResult {
14
15
  vendureConfig: VendureConfig;
@@ -16,7 +16,7 @@ const defaultPathAdapter = {
16
16
  * and in node_modules.
17
17
  */
18
18
  export async function compile(options) {
19
- var _a, _b;
19
+ var _a, _b, _c;
20
20
  const { vendureConfigPath, outputPath, pathAdapter, logger = noopLogger, pluginPackageScanner } = options;
21
21
  const getCompiledConfigPath = (_a = pathAdapter === null || pathAdapter === void 0 ? void 0 : pathAdapter.getCompiledConfigPath) !== null && _a !== void 0 ? _a : defaultPathAdapter.getCompiledConfigPath;
22
22
  const transformTsConfigPathMappings = (_b = pathAdapter === null || pathAdapter === void 0 ? void 0 : pathAdapter.transformTsConfigPathMappings) !== null && _b !== void 0 ? _b : defaultPathAdapter.transformTsConfigPathMappings;
@@ -29,6 +29,7 @@ export async function compile(options) {
29
29
  outputPath,
30
30
  logger,
31
31
  transformTsConfigPathMappings,
32
+ module: (_c = options.module) !== null && _c !== void 0 ? _c : 'commonjs',
32
33
  });
33
34
  logger.info(`TypeScript compilation completed in ${Date.now() - compileStart}ms`);
34
35
  // 2. Discover plugins
@@ -49,7 +50,7 @@ export async function compile(options) {
49
50
  configFileName,
50
51
  })).href.replace(/.ts$/, '.js');
51
52
  // Create package.json with type commonjs
52
- await fs.writeFile(path.join(outputPath, 'package.json'), JSON.stringify({ type: 'commonjs', private: true }, null, 2));
53
+ await fs.writeFile(path.join(outputPath, 'package.json'), JSON.stringify({ type: options.module === 'esm' ? 'module' : 'commonjs', private: true }, null, 2));
53
54
  // Find the exported config symbol
54
55
  const sourceFile = ts.createSourceFile(vendureConfigPath, await fs.readFile(vendureConfigPath, 'utf-8'), ts.ScriptTarget.Latest, true);
55
56
  const exportedSymbolName = findConfigExport(sourceFile);
@@ -80,14 +81,14 @@ export async function compile(options) {
80
81
  /**
81
82
  * Compiles TypeScript files to JavaScript
82
83
  */
83
- async function compileTypeScript({ inputPath, outputPath, logger, transformTsConfigPathMappings, }) {
84
+ async function compileTypeScript({ inputPath, outputPath, logger, transformTsConfigPathMappings, module, }) {
84
85
  var _a;
85
86
  await fs.ensureDir(outputPath);
86
87
  // Find tsconfig paths first
87
88
  const tsConfigInfo = await findTsConfigPaths(inputPath, logger, 'compiling', transformTsConfigPathMappings);
88
89
  const compilerOptions = {
89
90
  target: ts.ScriptTarget.ES2020,
90
- module: ts.ModuleKind.CommonJS,
91
+ module: module === 'esm' ? ts.ModuleKind.ESNext : ts.ModuleKind.CommonJS,
91
92
  moduleResolution: ts.ModuleResolutionKind.Node10, // More explicit CJS resolution
92
93
  experimentalDecorators: true,
93
94
  emitDecoratorMetadata: true,
@@ -0,0 +1,5 @@
1
+ import { PluginInfo } from '../types.js';
2
+ /**
3
+ * Returns an array of the paths to plugins, based on the info provided by the ConfigLoaderApi.
4
+ */
5
+ export declare function getDashboardPaths(pluginInfo: PluginInfo[]): string[];
@@ -0,0 +1,20 @@
1
+ import path from 'path';
2
+ /**
3
+ * Returns an array of the paths to plugins, based on the info provided by the ConfigLoaderApi.
4
+ */
5
+ export function getDashboardPaths(pluginInfo) {
6
+ var _a;
7
+ return ((_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.flatMap(({ dashboardEntryPath, sourcePluginPath, pluginPath }) => {
8
+ if (!dashboardEntryPath) {
9
+ return [];
10
+ }
11
+ const sourcePaths = [];
12
+ if (sourcePluginPath) {
13
+ sourcePaths.push(path.join(path.dirname(sourcePluginPath), path.dirname(dashboardEntryPath)));
14
+ }
15
+ if (pluginPath) {
16
+ sourcePaths.push(path.join(path.dirname(pluginPath), path.dirname(dashboardEntryPath)));
17
+ }
18
+ return sourcePaths;
19
+ }).filter(x => x != null)) !== null && _a !== void 0 ? _a : []);
20
+ }
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import { getDashboardPaths } from './utils/get-dashboard-paths.js';
2
2
  import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
3
3
  /**
4
4
  * This Vite plugin transforms the `app/styles.css` file to include a `@source` directive
@@ -16,25 +16,12 @@ export function dashboardTailwindSourcePlugin() {
16
16
  configLoaderApi = getConfigLoaderApi(plugins);
17
17
  },
18
18
  async transform(src, id) {
19
- var _a;
20
19
  if (/app\/styles.css$/.test(id)) {
21
20
  if (!loadVendureConfigResult) {
22
21
  loadVendureConfigResult = await configLoaderApi.getVendureConfig();
23
22
  }
24
23
  const { pluginInfo } = loadVendureConfigResult;
25
- const dashboardExtensionDirs = (_a = pluginInfo === null || pluginInfo === void 0 ? void 0 : pluginInfo.flatMap(({ dashboardEntryPath, sourcePluginPath, pluginPath }) => {
26
- if (!dashboardEntryPath) {
27
- return [];
28
- }
29
- const sourcePaths = [];
30
- if (sourcePluginPath) {
31
- sourcePaths.push(path.join(path.dirname(sourcePluginPath), path.dirname(dashboardEntryPath)));
32
- }
33
- if (pluginPath) {
34
- sourcePaths.push(path.join(path.dirname(pluginPath), path.dirname(dashboardEntryPath)));
35
- }
36
- return sourcePaths;
37
- }).filter(x => x != null)) !== null && _a !== void 0 ? _a : [];
24
+ const dashboardExtensionDirs = getDashboardPaths(pluginInfo);
38
25
  const sources = dashboardExtensionDirs
39
26
  .map(extension => {
40
27
  return `@source '${extension}';`;
@@ -16,7 +16,16 @@ export interface TranslationsPluginOptions {
16
16
  }
17
17
  /**
18
18
  * @description
19
- * This Vite plugin compiles
19
+ * This Vite plugin compiles the source .po files into JS bundles that can be loaded statically.
20
+ *
21
+ * It handles 2 modes: dev and build.
22
+ *
23
+ * - The dev case is handled in the `load` function using Vite virtual
24
+ * modules to compile and return translations from plugins _only_, which then get merged with the built-in
25
+ * translations in the `loadI18nMessages` function
26
+ * - The build case loads both built-in and plugin translations, merges them, and outputs the compiled
27
+ * files as .js files that can be statically consumed by the built app.
28
+ *
20
29
  * @param options
21
30
  */
22
31
  export declare function translationsPlugin(options: TranslationsPluginOptions): Plugin;
@@ -1,62 +1,82 @@
1
1
  import { createCompilationErrorMessage, createCompiledCatalog, getCatalogForFile, getCatalogs, } from '@lingui/cli/api';
2
2
  import { getConfig } from '@lingui/conf';
3
+ import glob from 'fast-glob';
3
4
  import * as fs from 'fs';
4
5
  import * as path from 'path';
6
+ import { getDashboardPaths } from './utils/get-dashboard-paths.js';
7
+ import { getConfigLoaderApi } from './vite-plugin-config-loader.js';
8
+ const virtualModuleId = 'virtual:plugin-translations';
9
+ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
5
10
  /**
6
11
  * @description
7
- * This Vite plugin compiles
12
+ * This Vite plugin compiles the source .po files into JS bundles that can be loaded statically.
13
+ *
14
+ * It handles 2 modes: dev and build.
15
+ *
16
+ * - The dev case is handled in the `load` function using Vite virtual
17
+ * modules to compile and return translations from plugins _only_, which then get merged with the built-in
18
+ * translations in the `loadI18nMessages` function
19
+ * - The build case loads both built-in and plugin translations, merges them, and outputs the compiled
20
+ * files as .js files that can be statically consumed by the built app.
21
+ *
8
22
  * @param options
9
23
  */
10
24
  export function translationsPlugin(options) {
11
- const { externalPoFiles = [], localesDir = 'src/i18n/locales', outputPath = 'assets/i18n' } = options;
12
- const linguiConfig = getConfig({ configPath: path.join(options.packageRoot, 'lingui.config.js') });
13
- const catalogsPromise = getCatalogs(linguiConfig);
14
- async function compileTranslations(files, emitFile) {
15
- const catalogs = await catalogsPromise;
16
- for (const file of files) {
17
- const catalogRelativePath = path.relative(options.packageRoot, file.path);
18
- const fileCatalog = getCatalogForFile(catalogRelativePath, catalogs);
19
- const { locale, catalog } = fileCatalog;
20
- const { messages } = await catalog.getTranslations(locale, {
21
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
22
- fallbackLocales: { default: linguiConfig.sourceLocale },
23
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
24
- sourceLocale: linguiConfig.sourceLocale,
25
- });
26
- const { source: code, errors } = createCompiledCatalog(locale, messages, {
27
- namespace: 'es',
28
- pseudoLocale: linguiConfig.pseudoLocale,
29
- });
30
- if (errors.length) {
31
- const message = createCompilationErrorMessage(locale, errors);
32
- throw new Error(message +
33
- `These errors fail build because \`failOnCompileError=true\` in Lingui Vite plugin configuration.`);
34
- }
35
- // Emit the compiled JavaScript file to the build output
36
- const outputFileName = path.posix.join(outputPath, `${locale}.js`);
37
- emitFile({
38
- type: 'asset',
39
- fileName: outputFileName,
40
- source: code,
41
- });
42
- }
43
- }
25
+ let configLoaderApi;
26
+ let loadVendureConfigResult;
44
27
  return {
45
28
  name: 'vendure:compile-translations',
29
+ configResolved({ plugins }) {
30
+ configLoaderApi = getConfigLoaderApi(plugins);
31
+ },
32
+ resolveId(id) {
33
+ if (id === virtualModuleId) {
34
+ return resolvedVirtualModuleId;
35
+ }
36
+ },
37
+ async load(id) {
38
+ if (id === resolvedVirtualModuleId) {
39
+ this.debug('Loading plugin translations...');
40
+ if (!loadVendureConfigResult) {
41
+ loadVendureConfigResult = await configLoaderApi.getVendureConfig();
42
+ }
43
+ const { pluginInfo } = loadVendureConfigResult;
44
+ const pluginTranslations = await getPluginTranslations(pluginInfo);
45
+ const linguiConfig = getConfig({
46
+ configPath: path.join(options.packageRoot, 'lingui.config.js'),
47
+ });
48
+ const catalogs = await getLinguiCatalogs(linguiConfig, pluginTranslations);
49
+ const pluginFiles = pluginTranslations.flatMap(translation => translation.translations);
50
+ const mergedMessageMap = await createMergedMessageMap({
51
+ files: pluginFiles,
52
+ packageRoot: options.packageRoot,
53
+ catalogs,
54
+ sourceLocale: linguiConfig.sourceLocale,
55
+ });
56
+ return `
57
+ const translations = {
58
+ ${[...mergedMessageMap.entries()]
59
+ .map(([locale, messages]) => {
60
+ const safeLocale = locale.replace(/-/g, '_');
61
+ return `${safeLocale}: ${JSON.stringify(messages)}`;
62
+ })
63
+ .join(',\n')}
64
+ };
65
+ export default translations;
66
+ `;
67
+ }
68
+ },
69
+ // This runs at build-time only
46
70
  async generateBundle() {
47
71
  // This runs during the bundle generation phase - emit files directly to build output
48
72
  try {
49
- const resolvedLocalesDir = path.resolve(options.packageRoot, localesDir);
50
- // Get all built-in .po files
51
- const builtInFiles = fs
52
- .readdirSync(resolvedLocalesDir)
53
- .filter(file => file.endsWith('.po'))
54
- .map(file => ({
55
- name: file,
56
- path: path.join(resolvedLocalesDir, file),
57
- }));
58
- await compileTranslations(builtInFiles, this.emitFile);
59
- this.info(`✓ Processed ${builtInFiles.length} translation files to ${outputPath}`);
73
+ const { pluginInfo } = await configLoaderApi.getVendureConfig();
74
+ // Get any plugin-provided .po files
75
+ const pluginTranslations = await getPluginTranslations(pluginInfo);
76
+ const pluginTranslationFiles = pluginTranslations.flatMap(p => p.translations);
77
+ this.info(`Found ${pluginTranslationFiles.length} translation files from plugins`);
78
+ this.debug(pluginTranslationFiles.join('\n'));
79
+ await compileTranslations(options, pluginTranslations, this.emitFile);
60
80
  }
61
81
  catch (error) {
62
82
  this.error(`Translation plugin error: ${error instanceof Error ? error.message : String(error)}`);
@@ -64,3 +84,94 @@ export function translationsPlugin(options) {
64
84
  },
65
85
  };
66
86
  }
87
+ async function getPluginTranslations(pluginInfo) {
88
+ const dashboardPaths = getDashboardPaths(pluginInfo);
89
+ const pluginTranslations = [];
90
+ for (const dashboardPath of dashboardPaths) {
91
+ const poPatterns = path.join(dashboardPath, '**/*.po');
92
+ const translations = await glob(poPatterns, {
93
+ ignore: [
94
+ // Standard test & doc files
95
+ '**/node_modules/**/node_modules/**',
96
+ '**/*.spec.js',
97
+ '**/*.test.js',
98
+ ],
99
+ onlyFiles: true,
100
+ absolute: true,
101
+ followSymbolicLinks: false,
102
+ stats: false,
103
+ });
104
+ pluginTranslations.push({
105
+ pluginRootPath: dashboardPath,
106
+ translations,
107
+ });
108
+ }
109
+ return pluginTranslations;
110
+ }
111
+ async function compileTranslations(options, pluginTranslations, emitFile) {
112
+ const { localesDir = 'src/i18n/locales', outputPath = 'assets/i18n' } = options;
113
+ const linguiConfig = getConfig({ configPath: path.join(options.packageRoot, 'lingui.config.js') });
114
+ const resolvedLocalesDir = path.resolve(options.packageRoot, localesDir);
115
+ const catalogs = await getLinguiCatalogs(linguiConfig, pluginTranslations);
116
+ // Get all built-in .po files
117
+ const builtInFiles = fs
118
+ .readdirSync(resolvedLocalesDir)
119
+ .filter(file => file.endsWith('.po'))
120
+ .map(file => path.join(resolvedLocalesDir, file));
121
+ const pluginFiles = pluginTranslations.flatMap(translation => translation.translations);
122
+ const mergedMessageMap = await createMergedMessageMap({
123
+ files: [...builtInFiles, ...pluginFiles],
124
+ packageRoot: options.packageRoot,
125
+ catalogs,
126
+ sourceLocale: linguiConfig.sourceLocale,
127
+ });
128
+ for (const [locale, messages] of mergedMessageMap.entries()) {
129
+ const { source: code, errors } = createCompiledCatalog(locale, messages, {
130
+ namespace: 'es',
131
+ pseudoLocale: linguiConfig.pseudoLocale,
132
+ });
133
+ if (errors.length) {
134
+ const message = createCompilationErrorMessage(locale, errors);
135
+ throw new Error(message +
136
+ `These errors fail build because \`failOnCompileError=true\` in Lingui Vite plugin configuration.`);
137
+ }
138
+ // Emit the compiled JavaScript file to the build output
139
+ const outputFileName = path.posix.join(outputPath, `${locale}.js`);
140
+ emitFile({
141
+ type: 'asset',
142
+ fileName: outputFileName,
143
+ source: code,
144
+ });
145
+ }
146
+ }
147
+ async function getLinguiCatalogs(linguiConfig, pluginTranslations) {
148
+ var _a, _b, _c;
149
+ for (const pluginTranslation of pluginTranslations) {
150
+ if (pluginTranslation.translations.length === 0) {
151
+ continue;
152
+ }
153
+ (_a = linguiConfig.catalogs) === null || _a === void 0 ? void 0 : _a.push({
154
+ path: (_c = (_b = pluginTranslation.translations[0]) === null || _b === void 0 ? void 0 : _b.replace(/[a-z_-]+\.po$/, '{locale}')) !== null && _c !== void 0 ? _c : '',
155
+ include: [],
156
+ });
157
+ }
158
+ return getCatalogs(linguiConfig);
159
+ }
160
+ async function createMergedMessageMap({ files, packageRoot, catalogs, sourceLocale, }) {
161
+ var _a;
162
+ const mergedMessageMap = new Map();
163
+ for (const file of files) {
164
+ const catalogRelativePath = path.relative(packageRoot, file);
165
+ const fileCatalog = getCatalogForFile(catalogRelativePath, catalogs);
166
+ const { locale, catalog } = fileCatalog;
167
+ const { messages } = await catalog.getTranslations(locale, {
168
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
169
+ fallbackLocales: { default: sourceLocale },
170
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
171
+ sourceLocale: sourceLocale,
172
+ });
173
+ const mergedMessages = (_a = mergedMessageMap.get(locale)) !== null && _a !== void 0 ? _a : {};
174
+ mergedMessageMap.set(locale, Object.assign(Object.assign({}, mergedMessages), messages));
175
+ }
176
+ return mergedMessageMap;
177
+ }
@@ -77,6 +77,18 @@ export type VitePluginVendureDashboardOptions = {
77
77
  * the location based on the location of the `@vendure/core` package.
78
78
  */
79
79
  pluginPackageScanner?: PackageScannerConfig;
80
+ /**
81
+ * @description
82
+ * Allows you to specify the module system to use when compiling and loading your Vendure config.
83
+ * By default, the compiler will use CommonJS, but you can set it to `esm` if you are using
84
+ * ES Modules in your Vendure project.
85
+ *
86
+ * **Status** Developer preview. If you are using ESM please try this out and provide us with feedback!
87
+ *
88
+ * @since 3.5.1
89
+ * @default 'commonjs'
90
+ */
91
+ module?: 'commonjs' | 'esm';
80
92
  /**
81
93
  * @description
82
94
  * Allows you to selectively disable individual plugins.
@@ -76,6 +76,7 @@ export function vendureDashboardPlugin(options) {
76
76
  outputPath: tempDir,
77
77
  pathAdapter: options.pathAdapter,
78
78
  pluginPackageScanner: options.pluginPackageScanner,
79
+ module: options.module,
79
80
  }),
80
81
  },
81
82
  {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.5.1-master-202511120232",
4
+ "version": "3.5.1-master-202511130232",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -155,8 +155,8 @@
155
155
  "@storybook/addon-vitest": "^10.0.0-beta.9",
156
156
  "@storybook/react-vite": "^10.0.0-beta.9",
157
157
  "@types/node": "^22.13.4",
158
- "@vendure/common": "^3.5.1-master-202511120232",
159
- "@vendure/core": "^3.5.1-master-202511120232",
158
+ "@vendure/common": "^3.5.1-master-202511130232",
159
+ "@vendure/core": "^3.5.1-master-202511130232",
160
160
  "@vitest/browser": "^3.2.4",
161
161
  "@vitest/coverage-v8": "^3.2.4",
162
162
  "eslint": "^9.19.0",
@@ -173,5 +173,5 @@
173
173
  "lightningcss-linux-arm64-musl": "^1.29.3",
174
174
  "lightningcss-linux-x64-musl": "^1.29.1"
175
175
  },
176
- "gitHead": "9361156a66b865340501d3ad1257addd09fb10f5"
176
+ "gitHead": "8b6e0be6580da59439a50299cfc1d0c7b65f34ec"
177
177
  }
@@ -1,13 +1,144 @@
1
- import { X } from 'lucide-react';
2
- import { KeyboardEvent, useId, useRef, useState } from 'react';
1
+ import { GripVertical, X } from 'lucide-react';
2
+ import { KeyboardEvent, useEffect, useId, useRef, useState } from 'react';
3
3
 
4
4
  import { Badge } from '@/vdb/components/ui/badge.js';
5
5
  import { Input } from '@/vdb/components/ui/input.js';
6
6
  import type { DashboardFormComponentProps } from '@/vdb/framework/form-engine/form-engine-types.js';
7
7
  import { isReadonlyField } from '@/vdb/framework/form-engine/utils.js';
8
8
  import { cn } from '@/vdb/lib/utils.js';
9
+ import {
10
+ closestCenter,
11
+ DndContext,
12
+ type DragEndEvent,
13
+ KeyboardSensor,
14
+ PointerSensor,
15
+ useSensor,
16
+ useSensors,
17
+ } from '@dnd-kit/core';
18
+ import {
19
+ arrayMove,
20
+ SortableContext,
21
+ sortableKeyboardCoordinates,
22
+ useSortable,
23
+ verticalListSortingStrategy,
24
+ } from '@dnd-kit/sortable';
25
+ import { CSS } from '@dnd-kit/utilities';
9
26
  import { useLingui } from '@lingui/react';
10
27
 
28
+ interface SortableItemProps {
29
+ id: string;
30
+ item: string;
31
+ isDisabled: boolean;
32
+ isEditing: boolean;
33
+ onRemove: () => void;
34
+ onEdit: () => void;
35
+ onSave: (newValue: string) => void;
36
+ }
37
+
38
+ function SortableItem({ id, item, isDisabled, isEditing, onRemove, onEdit, onSave }: SortableItemProps) {
39
+ const [editValue, setEditValue] = useState(item);
40
+ const inputRef = useRef<HTMLInputElement>(null);
41
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
42
+ id,
43
+ });
44
+
45
+ const style = {
46
+ transform: CSS.Transform.toString(transform),
47
+ transition,
48
+ };
49
+
50
+ const handleSave = () => {
51
+ const trimmedValue = editValue.trim();
52
+ if (trimmedValue && trimmedValue !== item) {
53
+ onSave(trimmedValue);
54
+ } else {
55
+ setEditValue(item);
56
+ onSave(item);
57
+ }
58
+ };
59
+
60
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
61
+ if (e.key === 'Enter') {
62
+ e.preventDefault();
63
+ handleSave();
64
+ } else if (e.key === 'Escape') {
65
+ setEditValue(item);
66
+ onSave(item);
67
+ }
68
+ };
69
+
70
+ // Focus and select input when entering edit mode
71
+ useEffect(() => {
72
+ if (isEditing && inputRef.current) {
73
+ inputRef.current.focus();
74
+ inputRef.current.select();
75
+ }
76
+ }, [isEditing]);
77
+
78
+ return (
79
+ <Badge
80
+ ref={setNodeRef}
81
+ style={style}
82
+ variant="secondary"
83
+ className={cn(
84
+ isDragging && 'opacity-50',
85
+ 'flex items-center gap-1',
86
+ isEditing && 'border-muted-foreground/30',
87
+ )}
88
+ >
89
+ {!isDisabled && (
90
+ <button
91
+ type="button"
92
+ className={cn(
93
+ 'cursor-grab active:cursor-grabbing text-muted-foreground',
94
+ 'hover:bg-muted rounded p-0.5',
95
+ )}
96
+ {...attributes}
97
+ {...listeners}
98
+ aria-label={`Drag ${item}`}
99
+ >
100
+ <GripVertical className="h-3 w-3" />
101
+ </button>
102
+ )}
103
+ {isEditing ? (
104
+ <input
105
+ ref={inputRef}
106
+ type="text"
107
+ value={editValue}
108
+ onChange={e => setEditValue(e.target.value)}
109
+ onKeyDown={handleKeyDown}
110
+ onBlur={handleSave}
111
+ className="bg-transparent border-none outline-none focus:ring-0 p-0 h-auto min-w-[60px] w-auto"
112
+ style={{ width: `${Math.max(editValue.length * 8, 60)}px` }}
113
+ />
114
+ ) : (
115
+ <span
116
+ onClick={!isDisabled ? onEdit : undefined}
117
+ className={cn(!isDisabled && 'cursor-text hover:underline')}
118
+ >
119
+ {item}
120
+ </span>
121
+ )}
122
+ {!isDisabled && (
123
+ <button
124
+ type="button"
125
+ onClick={e => {
126
+ e.stopPropagation();
127
+ onRemove();
128
+ }}
129
+ className={cn(
130
+ 'ml-1 rounded-full outline-none ring-offset-background text-muted-foreground',
131
+ 'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
132
+ )}
133
+ aria-label={`Remove ${item}`}
134
+ >
135
+ <X className="h-3 w-3" />
136
+ </button>
137
+ )}
138
+ </Badge>
139
+ );
140
+ }
141
+
11
142
  export function StringListInput({
12
143
  value,
13
144
  onChange,
@@ -17,13 +148,21 @@ export function StringListInput({
17
148
  fieldDef,
18
149
  }: DashboardFormComponentProps) {
19
150
  const [inputValue, setInputValue] = useState('');
151
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
20
152
  const inputRef = useRef<HTMLInputElement>(null);
21
153
  const { i18n } = useLingui();
22
- const isDisabled = isReadonlyField(fieldDef) || disabled;
154
+ const isDisabled = isReadonlyField(fieldDef) || disabled || false;
23
155
  const id = useId();
24
156
 
25
157
  const items = Array.isArray(value) ? value : [];
26
158
 
159
+ const sensors = useSensors(
160
+ useSensor(PointerSensor),
161
+ useSensor(KeyboardSensor, {
162
+ coordinateGetter: sortableKeyboardCoordinates,
163
+ }),
164
+ );
165
+
27
166
  const addItem = (item: string) => {
28
167
  const trimmedItem = item.trim();
29
168
  if (trimmedItem) {
@@ -36,6 +175,24 @@ export function StringListInput({
36
175
  onChange(items.filter((_, index) => index !== indexToRemove));
37
176
  };
38
177
 
178
+ const editItem = (index: number, newValue: string) => {
179
+ const newItems = [...items];
180
+ newItems[index] = newValue;
181
+ onChange(newItems);
182
+ setEditingIndex(null);
183
+ };
184
+
185
+ const handleDragEnd = (event: DragEndEvent) => {
186
+ const { active, over } = event;
187
+
188
+ if (over && active.id !== over.id) {
189
+ const oldIndex = items.findIndex((_, idx) => `${id}-${idx}` === active.id);
190
+ const newIndex = items.findIndex((_, idx) => `${id}-${idx}` === over.id);
191
+
192
+ onChange(arrayMove(items, oldIndex, newIndex));
193
+ }
194
+ };
195
+
39
196
  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
40
197
  if (e.key === 'Enter' || e.key === ',') {
41
198
  e.preventDefault();
@@ -74,29 +231,27 @@ export function StringListInput({
74
231
  className="min-w-[120px]"
75
232
  />
76
233
  )}
77
- <div className="flex flex-wrap gap-1 items-start justify-start">
78
- {items.map((item, index) => (
79
- <Badge key={id + index} variant="secondary">
80
- <span>{item}</span>
81
- {!isDisabled && (
82
- <button
83
- type="button"
84
- onClick={e => {
85
- e.stopPropagation();
86
- removeItem(index);
87
- }}
88
- className={cn(
89
- 'ml-1 rounded-full outline-none ring-offset-background',
90
- 'hover:bg-muted focus:ring-2 focus:ring-ring focus:ring-offset-2',
91
- )}
92
- aria-label={`Remove ${item}`}
93
- >
94
- <X className="h-3 w-3" />
95
- </button>
96
- )}
97
- </Badge>
98
- ))}
99
- </div>
234
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
235
+ <SortableContext
236
+ items={items.map((_, index) => `${id}-${index}`)}
237
+ strategy={verticalListSortingStrategy}
238
+ >
239
+ <div className="flex flex-wrap gap-1 items-start justify-start">
240
+ {items.map((item, index) => (
241
+ <SortableItem
242
+ key={`${id}-${index}`}
243
+ id={`${id}-${index}`}
244
+ item={item}
245
+ isDisabled={isDisabled}
246
+ isEditing={editingIndex === index}
247
+ onRemove={() => removeItem(index)}
248
+ onEdit={() => setEditingIndex(index)}
249
+ onSave={newValue => editItem(index, newValue)}
250
+ />
251
+ ))}
252
+ </div>
253
+ </SortableContext>
254
+ </DndContext>
100
255
  </div>
101
256
  );
102
257
  }
@@ -72,7 +72,7 @@ export function CustomFieldsForm({ entityType, control, formPathPrefix }: Readon
72
72
  const shouldShowTabs = useMemo(() => {
73
73
  if (!customFields) return false;
74
74
  const hasTabbedFields = customFields.some(field => field.ui?.tab);
75
- return hasTabbedFields || groupedFields.length > 1;
75
+ return hasTabbedFields && groupedFields.length > 1;
76
76
  }, [customFields, groupedFields.length]);
77
77
 
78
78
  if (!shouldShowTabs) {
@@ -12,6 +12,9 @@ export async function loadI18nMessages(locale: string): Promise<Messages> {
12
12
  } else {
13
13
  // In dev mode we allow the dynamic import behaviour
14
14
  const { messages } = await import(`../../i18n/locales/${locale}.po`);
15
- return messages;
15
+ const pluginTranslations = await import('virtual:plugin-translations');
16
+ const safeLocale = locale.replace(/-/g, '_');
17
+ const pluginTranslationsForLocale = pluginTranslations.default[safeLocale] ?? {};
18
+ return { ...messages, ...pluginTranslationsForLocale };
16
19
  }
17
20
  }
@@ -5,6 +5,9 @@ declare module 'virtual:admin-api-schema' {
5
5
  declare module 'virtual:dashboard-extensions' {
6
6
  export const runDashboardExtensions: () => Promise<void>;
7
7
  }
8
+ declare module 'virtual:plugin-translations' {
9
+ export default translations = Record<string, any>;
10
+ }
8
11
 
9
12
  declare module 'virtual:vendure-ui-config' {
10
13
  import { LanguageCode } from '@vendure/core';