@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.
- package/dist/vite/utils/compiler.d.ts +1 -0
- package/dist/vite/utils/compiler.js +5 -4
- package/dist/vite/utils/get-dashboard-paths.d.ts +5 -0
- package/dist/vite/utils/get-dashboard-paths.js +20 -0
- package/dist/vite/vite-plugin-tailwind-source.js +2 -15
- package/dist/vite/vite-plugin-translations.d.ts +10 -1
- package/dist/vite/vite-plugin-translations.js +156 -45
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +12 -0
- package/dist/vite/vite-plugin-vendure-dashboard.js +1 -0
- package/package.json +4 -4
- package/src/lib/components/data-input/string-list-input.tsx +181 -26
- package/src/lib/components/shared/custom-fields-form.tsx +1 -1
- package/src/lib/lib/load-i18n-messages.ts +4 -1
- package/src/lib/virtual.d.ts +3 -0
|
@@ -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,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
|
|
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 = (
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
50
|
-
// Get
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
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-
|
|
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-
|
|
159
|
-
"@vendure/core": "^3.5.1-master-
|
|
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": "
|
|
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
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/lib/virtual.d.ts
CHANGED
|
@@ -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';
|