@haus-tech/product-import-export-plugin 2.2.0-dev.0 → 2.2.0-dev.2
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/api/product-import.controller.d.ts +2 -2
- package/dist/api/product-import.controller.js +5 -4
- package/dist/providers/import-providers/asset-importer.js +13 -2
- package/dist/providers/import-providers/product-importer.d.ts +3 -3
- package/dist/providers/import-providers/product-importer.js +26 -9
- package/dist/services/extended-fast-importer.service.d.ts +5 -2
- package/dist/services/extended-fast-importer.service.js +65 -21
- package/dist/services/product-import.service.d.ts +3 -2
- package/dist/services/product-import.service.js +5 -4
- package/dist/types.d.ts +1 -0
- package/dist/ui/components/export-dialog.component.html +1 -1
- package/dist/ui/components/product-import.component.d.ts +2 -0
- package/dist/ui/components/product-import.component.html +49 -2
- package/dist/ui/components/product-import.component.js +13 -1
- package/dist/ui/components/product-import.component.ts +22 -1
- package/dist/ui/translations/en.json +11 -3
- package/dist/ui/translations/sv.json +11 -3
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/// <reference types="Multer" />
|
|
2
2
|
import { LanguageCode, RequestContext } from '@vendure/core';
|
|
3
3
|
import { Response } from 'express';
|
|
4
|
-
import { PluginInitOptions } from '../types';
|
|
4
|
+
import { PluginInitOptions, UpdatingStrategy } from '../types';
|
|
5
5
|
import { ProductImportService } from '../services/product-import.service';
|
|
6
6
|
export declare class ProductImportController {
|
|
7
7
|
private options;
|
|
8
8
|
private service;
|
|
9
9
|
constructor(options: PluginInitOptions, service: ProductImportService);
|
|
10
|
-
updateProducts(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: "true" | "false" | undefined, mainLanguage: LanguageCode, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
10
|
+
updateProducts(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: "true" | "false" | undefined, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
11
11
|
}
|
|
@@ -26,7 +26,7 @@ let ProductImportController = class ProductImportController {
|
|
|
26
26
|
this.service = service;
|
|
27
27
|
}
|
|
28
28
|
async updateProducts(ctx, file, updateProductSlug = this.options.importOptions.defaultOptions
|
|
29
|
-
?.updateProductSlug || 'true', mainLanguage, res) {
|
|
29
|
+
?.updateProductSlug || 'true', mainLanguage, updatingStrategy, res) {
|
|
30
30
|
if (!file) {
|
|
31
31
|
return res.status(400).send('No file uploaded');
|
|
32
32
|
}
|
|
@@ -34,7 +34,7 @@ let ProductImportController = class ProductImportController {
|
|
|
34
34
|
return res.status(400).send('No mainLanguage provided');
|
|
35
35
|
}
|
|
36
36
|
try {
|
|
37
|
-
this.service.processFile(ctx, file, updateProductSlug === 'true', mainLanguage);
|
|
37
|
+
this.service.processFile(ctx, file, updateProductSlug === 'true', mainLanguage, updatingStrategy);
|
|
38
38
|
return res.status(200).send('File uploaded successfully');
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
@@ -49,9 +49,10 @@ __decorate([
|
|
|
49
49
|
__param(1, (0, common_1.UploadedFile)()),
|
|
50
50
|
__param(2, (0, common_1.Body)('updateProductSlug')),
|
|
51
51
|
__param(3, (0, common_1.Body)('mainLanguage')),
|
|
52
|
-
__param(4, (0, common_1.
|
|
52
|
+
__param(4, (0, common_1.Body)('updatingStrategy')),
|
|
53
|
+
__param(5, (0, common_1.Res)()),
|
|
53
54
|
__metadata("design:type", Function),
|
|
54
|
-
__metadata("design:paramtypes", [core_1.RequestContext, Object, String, String, Object]),
|
|
55
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object, String, String, String, Object]),
|
|
55
56
|
__metadata("design:returntype", Promise)
|
|
56
57
|
], ProductImportController.prototype, "updateProducts", null);
|
|
57
58
|
ProductImportController = __decorate([
|
|
@@ -45,12 +45,16 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
|
|
|
45
45
|
for (const assetPath of uniqueAssetPaths.values()) {
|
|
46
46
|
const cachedAsset = this.assetMap.get(assetPath.url);
|
|
47
47
|
if (cachedAsset) {
|
|
48
|
-
if (
|
|
48
|
+
if (cachedAsset.name !== assetPath.name && assetPath.name) {
|
|
49
49
|
this.connection.getRepository(ctx, core_1.Asset).update(cachedAsset.id, {
|
|
50
50
|
name: assetPath.name,
|
|
51
51
|
});
|
|
52
|
+
this.assetMap.set(assetPath.url, { ...cachedAsset, name: assetPath.name });
|
|
53
|
+
assets.push({ ...cachedAsset, name: assetPath.name });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
assets.push(cachedAsset);
|
|
52
57
|
}
|
|
53
|
-
assets.push(cachedAsset);
|
|
54
58
|
}
|
|
55
59
|
else {
|
|
56
60
|
try {
|
|
@@ -65,6 +69,12 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
|
|
|
65
69
|
},
|
|
66
70
|
});
|
|
67
71
|
if (assetFromHash) {
|
|
72
|
+
// Check if the asset name has changed
|
|
73
|
+
if (assetPath.name && assetFromHash.name !== assetPath.name) {
|
|
74
|
+
await this.connection.getRepository(ctx, core_1.Asset).update(assetFromHash.id, {
|
|
75
|
+
name: assetPath.name,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
68
78
|
this.assetMap.set(assetPath.url, assetFromHash);
|
|
69
79
|
assets.push(assetFromHash);
|
|
70
80
|
continue;
|
|
@@ -89,6 +99,7 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
|
|
|
89
99
|
shouldDeleteCreatedAsset = true;
|
|
90
100
|
assetToUpdate = { ...assetFromHash, name: assetPath.name || assetFromHash.name };
|
|
91
101
|
}
|
|
102
|
+
console.log('assetFromHash', assetFromHash);
|
|
92
103
|
if (assetPath.id) {
|
|
93
104
|
shouldDeleteCreatedAsset = true;
|
|
94
105
|
assetToUpdate = {
|
|
@@ -3,7 +3,7 @@ import { ConfigService, ChannelService, FacetService, FacetValueService, TaxCate
|
|
|
3
3
|
import { Stream } from 'stream';
|
|
4
4
|
import { Observable } from 'rxjs';
|
|
5
5
|
import { ExtendedFastImporterService } from '../../services/extended-fast-importer.service';
|
|
6
|
-
import { ParsedProductWithId } from '../../types';
|
|
6
|
+
import { ParsedProductWithId, UpdatingStrategy } from '../../types';
|
|
7
7
|
import { ImportParser } from './import-parser';
|
|
8
8
|
import { ExtendedAssetImporter } from './asset-importer';
|
|
9
9
|
export declare class ProductImporter {
|
|
@@ -26,7 +26,7 @@ export declare class ProductImporter {
|
|
|
26
26
|
private customFieldTypes;
|
|
27
27
|
private allAssets;
|
|
28
28
|
constructor(connection: TransactionalConnection, configService: ConfigService, importParser: ImportParser, assetImporter: ExtendedAssetImporter, fastImporter: ExtendedFastImporterService, translatableSaver: TranslatableSaver, productService: ProductService, channelService: ChannelService, facetService: FacetService, facetValueService: FacetValueService, taxCategoryService: TaxCategoryService, assetService: AssetService, optionGroupService: ProductOptionGroupService);
|
|
29
|
-
parseAndImport(input: string | Stream, ctx: RequestContext, updateProductSlug: boolean | undefined, mainLanguage: LanguageCode): Observable<ImportProgress>;
|
|
29
|
+
parseAndImport(input: string | Stream, ctx: RequestContext, updateProductSlug: boolean | undefined, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy): Observable<ImportProgress>;
|
|
30
30
|
private extractProductIdsFromCSV;
|
|
31
31
|
private preprocessCsv;
|
|
32
32
|
private streamToString;
|
|
@@ -34,7 +34,7 @@ export declare class ProductImporter {
|
|
|
34
34
|
setMissingAssetHashes(ctx: RequestContext): Promise<void>;
|
|
35
35
|
private doParseAndImport;
|
|
36
36
|
private getRequestContext;
|
|
37
|
-
importProducts(ctx: RequestContext, rows: ParsedProductWithId[], onProgress: OnProgressFn): Promise<string[]>;
|
|
37
|
+
importProducts(ctx: RequestContext, rows: ParsedProductWithId[], updatingStrategy: UpdatingStrategy, onProgress: OnProgressFn): Promise<string[]>;
|
|
38
38
|
private getFacetValueIds;
|
|
39
39
|
private processCustomFieldValues;
|
|
40
40
|
/**
|
|
@@ -56,9 +56,9 @@ let ProductImporter = class ProductImporter {
|
|
|
56
56
|
this.assetService = assetService;
|
|
57
57
|
this.optionGroupService = optionGroupService;
|
|
58
58
|
}
|
|
59
|
-
parseAndImport(input, ctx, updateProductSlug = true, mainLanguage) {
|
|
59
|
+
parseAndImport(input, ctx, updateProductSlug = true, mainLanguage, updatingStrategy) {
|
|
60
60
|
return new rxjs_1.Observable((subscriber) => {
|
|
61
|
-
const p = this.doParseAndImport(input, updateProductSlug, mainLanguage, ctx, (progress) => {
|
|
61
|
+
const p = this.doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, ctx, (progress) => {
|
|
62
62
|
subscriber.next(progress);
|
|
63
63
|
}).then((value) => {
|
|
64
64
|
subscriber.next({ ...value, currentProduct: 'Complete' });
|
|
@@ -169,7 +169,7 @@ let ProductImporter = class ProductImporter {
|
|
|
169
169
|
}
|
|
170
170
|
this.allAssets = assets;
|
|
171
171
|
}
|
|
172
|
-
async doParseAndImport(input, updateProductSlug, mainLanguage, reqCtx, onProgress) {
|
|
172
|
+
async doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, reqCtx, onProgress) {
|
|
173
173
|
const ctx = await this.getRequestContext(reqCtx, mainLanguage);
|
|
174
174
|
await this.setMissingAssetHashes(ctx);
|
|
175
175
|
let csvContent;
|
|
@@ -194,7 +194,7 @@ let ProductImporter = class ProductImporter {
|
|
|
194
194
|
});
|
|
195
195
|
if (parsed && parsed.results.length) {
|
|
196
196
|
try {
|
|
197
|
-
const importErrors = await this.importProducts(ctx, parsed.results, (progess) => {
|
|
197
|
+
const importErrors = await this.importProducts(ctx, parsed.results, updatingStrategy, (progess) => {
|
|
198
198
|
onProgress({
|
|
199
199
|
...progess,
|
|
200
200
|
processed: parsed.processed,
|
|
@@ -236,7 +236,7 @@ let ProductImporter = class ProductImporter {
|
|
|
236
236
|
throw new core_1.InternalServerError('No RequestContext provided');
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
async importProducts(ctx, rows, onProgress) {
|
|
239
|
+
async importProducts(ctx, rows, updatingStrategy, onProgress) {
|
|
240
240
|
let errors = [];
|
|
241
241
|
let imported = 0;
|
|
242
242
|
const languageCode = ctx.languageCode;
|
|
@@ -294,6 +294,7 @@ let ProductImporter = class ProductImporter {
|
|
|
294
294
|
'variants',
|
|
295
295
|
'variants.facetValues',
|
|
296
296
|
'assets',
|
|
297
|
+
'featuredAsset',
|
|
297
298
|
],
|
|
298
299
|
})
|
|
299
300
|
: undefined;
|
|
@@ -322,10 +323,17 @@ let ProductImporter = class ProductImporter {
|
|
|
322
323
|
previousNormalizedName: (0, normalize_string_1.normalizeString)(existingProductName),
|
|
323
324
|
}
|
|
324
325
|
: undefined;
|
|
326
|
+
if (existingProduct && updatingStrategy === 'replace') {
|
|
327
|
+
await this.fastImporter.removeAllAssetsFromProduct(existingProduct);
|
|
328
|
+
}
|
|
325
329
|
const productData = {
|
|
326
|
-
featuredAssetId:
|
|
330
|
+
featuredAssetId: existingProduct?.featuredAsset
|
|
331
|
+
? undefined
|
|
332
|
+
: productAssets.length
|
|
333
|
+
? productAssets[0].id
|
|
334
|
+
: undefined,
|
|
327
335
|
assetIds: productAssets.map((a) => a.id),
|
|
328
|
-
|
|
336
|
+
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
329
337
|
translations: await Promise.all(product.translations.map(async (translation) => {
|
|
330
338
|
return {
|
|
331
339
|
languageCode: translation.languageCode,
|
|
@@ -430,6 +438,7 @@ let ProductImporter = class ProductImporter {
|
|
|
430
438
|
sku: variant.sku,
|
|
431
439
|
},
|
|
432
440
|
withDeleted: true,
|
|
441
|
+
relations: ['facetValues', 'facetValues.facet', 'assets', 'featuredAsset'],
|
|
433
442
|
});
|
|
434
443
|
if (existingVariant?.deletedAt) {
|
|
435
444
|
existingVariant.deletedAt = null;
|
|
@@ -441,10 +450,18 @@ let ProductImporter = class ProductImporter {
|
|
|
441
450
|
console.error('Error while restoring the variant:', error);
|
|
442
451
|
}
|
|
443
452
|
}
|
|
453
|
+
if (existingVariant && updatingStrategy === 'replace') {
|
|
454
|
+
// Remove all assets from the product
|
|
455
|
+
await this.fastImporter.removeAllAssetsFromVariant(existingVariant);
|
|
456
|
+
}
|
|
444
457
|
const variantData = {
|
|
445
458
|
productId: createdProductId,
|
|
446
|
-
|
|
447
|
-
featuredAssetId:
|
|
459
|
+
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
460
|
+
featuredAssetId: existingVariant?.featuredAsset
|
|
461
|
+
? undefined
|
|
462
|
+
: variantAssets.length
|
|
463
|
+
? variantAssets[0].id
|
|
464
|
+
: undefined,
|
|
448
465
|
assetIds: variantAssets.map((a) => a.id),
|
|
449
466
|
sku: variant.sku,
|
|
450
467
|
taxCategoryId: this.getMatchingTaxCategoryId(variant.taxCategory, taxCategories.items),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Channel, ChannelService, ID, RequestContextService, StockMovementService, TransactionalConnection, TranslatableSaver } from '@vendure/core';
|
|
1
|
+
import { Channel, ChannelService, EntityHydrator, ID, Product, ProductVariant, RequestContextService, StockMovementService, TransactionalConnection, TranslatableSaver } from '@vendure/core';
|
|
2
2
|
import { CreateFacetInput, CreateFacetValueInput, CreateProductInput, CreateProductOptionGroupInput, CreateProductOptionInput, CreateProductVariantInput } from '@vendure/common/lib/generated-types';
|
|
3
3
|
export declare class ExtendedFastImporterService {
|
|
4
4
|
private connection;
|
|
@@ -6,10 +6,11 @@ export declare class ExtendedFastImporterService {
|
|
|
6
6
|
private stockMovementService;
|
|
7
7
|
private translatableSaver;
|
|
8
8
|
private requestContextService;
|
|
9
|
+
private entityHydrator;
|
|
9
10
|
private defaultChannel;
|
|
10
11
|
private importCtx;
|
|
11
12
|
/** @internal */
|
|
12
|
-
constructor(connection: TransactionalConnection, channelService: ChannelService, stockMovementService: StockMovementService, translatableSaver: TranslatableSaver, requestContextService: RequestContextService);
|
|
13
|
+
constructor(connection: TransactionalConnection, channelService: ChannelService, stockMovementService: StockMovementService, translatableSaver: TranslatableSaver, requestContextService: RequestContextService, entityHydrator: EntityHydrator);
|
|
13
14
|
/**
|
|
14
15
|
* @description
|
|
15
16
|
* This should be called prior to any of the import methods, as it establishes the
|
|
@@ -22,6 +23,8 @@ export declare class ExtendedFastImporterService {
|
|
|
22
23
|
updateProduct(id: ID, input: CreateProductInput & {
|
|
23
24
|
id: ID;
|
|
24
25
|
}): Promise<ID>;
|
|
26
|
+
removeAllAssetsFromProduct(product: Product): Promise<void>;
|
|
27
|
+
removeAllAssetsFromVariant(variant: ProductVariant): Promise<void>;
|
|
25
28
|
createProduct(input: CreateProductInput): Promise<ID>;
|
|
26
29
|
updateProductOptionGroup(input: CreateProductOptionGroupInput & {
|
|
27
30
|
id: ID;
|
|
@@ -21,15 +21,17 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
21
21
|
stockMovementService;
|
|
22
22
|
translatableSaver;
|
|
23
23
|
requestContextService;
|
|
24
|
+
entityHydrator;
|
|
24
25
|
defaultChannel;
|
|
25
26
|
importCtx;
|
|
26
27
|
/** @internal */
|
|
27
|
-
constructor(connection, channelService, stockMovementService, translatableSaver, requestContextService) {
|
|
28
|
+
constructor(connection, channelService, stockMovementService, translatableSaver, requestContextService, entityHydrator) {
|
|
28
29
|
this.connection = connection;
|
|
29
30
|
this.channelService = channelService;
|
|
30
31
|
this.stockMovementService = stockMovementService;
|
|
31
32
|
this.translatableSaver = translatableSaver;
|
|
32
33
|
this.requestContextService = requestContextService;
|
|
34
|
+
this.entityHydrator = entityHydrator;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* @description
|
|
@@ -59,7 +61,11 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
59
61
|
entityType: core_1.Product,
|
|
60
62
|
translationType: core_1.ProductTranslation,
|
|
61
63
|
beforeSave: async (p) => {
|
|
62
|
-
|
|
64
|
+
await this.entityHydrator.hydrate(this.importCtx, p, { relations: ['channels'] });
|
|
65
|
+
p.channels = (0, unique_1.unique)([...p.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
66
|
+
if (input.facetValueIds) {
|
|
67
|
+
p.facetValues = input.facetValueIds.map((id) => ({ id }));
|
|
68
|
+
}
|
|
63
69
|
if (input.featuredAssetId) {
|
|
64
70
|
p.featuredAsset = { id: input.featuredAssetId };
|
|
65
71
|
}
|
|
@@ -80,6 +86,20 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
80
86
|
}
|
|
81
87
|
return product.id;
|
|
82
88
|
}
|
|
89
|
+
async removeAllAssetsFromProduct(product) {
|
|
90
|
+
this.ensureInitialized();
|
|
91
|
+
await this.connection
|
|
92
|
+
.getRepository(this.importCtx, core_1.ProductAsset)
|
|
93
|
+
.delete({ productId: product.id });
|
|
94
|
+
product.featuredAsset = undefined;
|
|
95
|
+
}
|
|
96
|
+
async removeAllAssetsFromVariant(variant) {
|
|
97
|
+
this.ensureInitialized();
|
|
98
|
+
await this.connection
|
|
99
|
+
.getRepository(this.importCtx, core_1.ProductVariantAsset)
|
|
100
|
+
.delete({ productVariantId: variant.id });
|
|
101
|
+
variant.featuredAsset = undefined;
|
|
102
|
+
}
|
|
83
103
|
async createProduct(input) {
|
|
84
104
|
this.ensureInitialized();
|
|
85
105
|
// https://github.com/vendure-ecommerce/vendure/issues/2053
|
|
@@ -197,8 +217,9 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
197
217
|
input: { ...input, id: facetExists.id },
|
|
198
218
|
entityType: core_1.Facet,
|
|
199
219
|
translationType: core_1.FacetTranslation,
|
|
200
|
-
beforeSave: (facet) => {
|
|
201
|
-
|
|
220
|
+
beforeSave: async (facet) => {
|
|
221
|
+
await this.entityHydrator.hydrate(this.importCtx, facet, { relations: ['channels'] });
|
|
222
|
+
facet.channels = (0, unique_1.unique)([...facet.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
202
223
|
},
|
|
203
224
|
});
|
|
204
225
|
return facet.id;
|
|
@@ -208,7 +229,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
208
229
|
input,
|
|
209
230
|
entityType: core_1.Facet,
|
|
210
231
|
translationType: core_1.FacetTranslation,
|
|
211
|
-
beforeSave: (facet) => {
|
|
232
|
+
beforeSave: async (facet) => {
|
|
212
233
|
facet.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
213
234
|
},
|
|
214
235
|
});
|
|
@@ -228,9 +249,10 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
228
249
|
input: { ...input, id: facetValueExists.id },
|
|
229
250
|
entityType: core_1.FacetValue,
|
|
230
251
|
translationType: core_1.FacetValueTranslation,
|
|
231
|
-
beforeSave: (facetValue) => {
|
|
252
|
+
beforeSave: async (facetValue) => {
|
|
253
|
+
await this.entityHydrator.hydrate(this.importCtx, facetValue, { relations: ['channels'] });
|
|
232
254
|
facetValue.facet = { id: input.facetId };
|
|
233
|
-
facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
255
|
+
facetValue.channels = (0, unique_1.unique)([...facetValue.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
234
256
|
},
|
|
235
257
|
});
|
|
236
258
|
return updatedFacetValue.id;
|
|
@@ -240,7 +262,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
240
262
|
input,
|
|
241
263
|
entityType: core_1.FacetValue,
|
|
242
264
|
translationType: core_1.FacetValueTranslation,
|
|
243
|
-
beforeSave: (facetValue) => {
|
|
265
|
+
beforeSave: async (facetValue) => {
|
|
244
266
|
facetValue.facet = { id: input.facetId };
|
|
245
267
|
facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
246
268
|
},
|
|
@@ -299,11 +321,15 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
299
321
|
entityType: core_1.ProductVariant,
|
|
300
322
|
translationType: core_1.ProductVariantTranslation,
|
|
301
323
|
beforeSave: async (variant) => {
|
|
302
|
-
|
|
324
|
+
await this.entityHydrator.hydrate(this.importCtx, variant, { relations: ['channels'] });
|
|
325
|
+
variant.channels = (0, unique_1.unique)([...variant.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
303
326
|
const { optionIds } = input;
|
|
304
327
|
if (optionIds && optionIds.length) {
|
|
305
328
|
variant.options = optionIds.map((id) => ({ id }));
|
|
306
329
|
}
|
|
330
|
+
if (input.facetValueIds) {
|
|
331
|
+
variant.facetValues = input.facetValueIds.map((id) => ({ id }));
|
|
332
|
+
}
|
|
307
333
|
variant.product = { id: input.productId };
|
|
308
334
|
variant.taxCategory = { id: input.taxCategoryId };
|
|
309
335
|
if (input.featuredAssetId) {
|
|
@@ -331,15 +357,32 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
331
357
|
await this.stockMovementService.adjustProductVariantStock(this.importCtx, updatedVariant.id, input.stockOnHand ?? 0);
|
|
332
358
|
const assignedChannelIds = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id').map((c) => c.id);
|
|
333
359
|
for (const channelId of assignedChannelIds) {
|
|
334
|
-
const
|
|
335
|
-
price: input.price,
|
|
336
|
-
channelId,
|
|
337
|
-
currencyCode: this.defaultChannel.defaultCurrencyCode,
|
|
338
|
-
});
|
|
339
|
-
variantPrice.variant = updatedVariant;
|
|
340
|
-
await this.connection
|
|
360
|
+
const allPrices = await this.connection
|
|
341
361
|
.getRepository(this.importCtx, core_1.ProductVariantPrice)
|
|
342
|
-
.
|
|
362
|
+
.find({
|
|
363
|
+
where: {
|
|
364
|
+
variant: { id: updatedVariant.id },
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
let existingVariantPrice = allPrices.find((p) => (0, core_1.idsAreEqual)(p.channelId, channelId) &&
|
|
368
|
+
p.currencyCode === this.importCtx.channel.defaultCurrencyCode);
|
|
369
|
+
if (!existingVariantPrice) {
|
|
370
|
+
const variantPrice = new core_1.ProductVariantPrice({
|
|
371
|
+
price: input.price,
|
|
372
|
+
channelId,
|
|
373
|
+
currencyCode: this.importCtx.channel.defaultCurrencyCode,
|
|
374
|
+
});
|
|
375
|
+
variantPrice.variant = updatedVariant;
|
|
376
|
+
await this.connection.rawConnection
|
|
377
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
378
|
+
.save(variantPrice, { reload: false });
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
existingVariantPrice.price = input.price;
|
|
382
|
+
await this.connection.rawConnection
|
|
383
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
384
|
+
.save(existingVariantPrice, { reload: false });
|
|
385
|
+
}
|
|
343
386
|
}
|
|
344
387
|
return updatedVariant.id;
|
|
345
388
|
}
|
|
@@ -392,11 +435,11 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
392
435
|
const variantPrice = new core_1.ProductVariantPrice({
|
|
393
436
|
price: input.price,
|
|
394
437
|
channelId,
|
|
395
|
-
currencyCode: this.
|
|
438
|
+
currencyCode: this.importCtx.channel.defaultCurrencyCode,
|
|
396
439
|
});
|
|
397
440
|
variantPrice.variant = createdVariant;
|
|
398
|
-
await this.connection
|
|
399
|
-
.getRepository(
|
|
441
|
+
await this.connection.rawConnection
|
|
442
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
400
443
|
.save(variantPrice, { reload: false });
|
|
401
444
|
}
|
|
402
445
|
return createdVariant.id;
|
|
@@ -413,6 +456,7 @@ ExtendedFastImporterService = __decorate([
|
|
|
413
456
|
core_1.ChannelService,
|
|
414
457
|
core_1.StockMovementService,
|
|
415
458
|
core_1.TranslatableSaver,
|
|
416
|
-
core_1.RequestContextService
|
|
459
|
+
core_1.RequestContextService,
|
|
460
|
+
core_1.EntityHydrator])
|
|
417
461
|
], ExtendedFastImporterService);
|
|
418
462
|
exports.ExtendedFastImporterService = ExtendedFastImporterService;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/// <reference types="Multer" />
|
|
2
2
|
import { OnModuleInit } from '@nestjs/common';
|
|
3
3
|
import { RequestContext, JobQueueService, SerializedRequestContext, SearchService, LanguageCode } from '@vendure/core';
|
|
4
|
-
import { PluginInitOptions } from '../types';
|
|
4
|
+
import { PluginInitOptions, UpdatingStrategy } from '../types';
|
|
5
5
|
import { ProductImporter } from '../providers/import-providers/product-importer';
|
|
6
6
|
export declare class ProductImportService implements OnModuleInit {
|
|
7
7
|
private options;
|
|
@@ -11,11 +11,12 @@ export declare class ProductImportService implements OnModuleInit {
|
|
|
11
11
|
private productImportQueue;
|
|
12
12
|
constructor(options: PluginInitOptions, jobQueueService: JobQueueService, productImporter: ProductImporter, searchService: SearchService);
|
|
13
13
|
onModuleInit(): Promise<void>;
|
|
14
|
-
processFile(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: boolean, mainLanguage: LanguageCode): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<{
|
|
14
|
+
processFile(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: boolean, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<{
|
|
15
15
|
ctx: SerializedRequestContext;
|
|
16
16
|
fileContent: string;
|
|
17
17
|
updateProductSlug: boolean;
|
|
18
18
|
mainLanguage: LanguageCode;
|
|
19
|
+
updatingStrategy: UpdatingStrategy;
|
|
19
20
|
}>>;
|
|
20
21
|
private triggerProductImport;
|
|
21
22
|
}
|
|
@@ -39,7 +39,7 @@ let ProductImportService = class ProductImportService {
|
|
|
39
39
|
let jobResult;
|
|
40
40
|
return new Promise((resolve, reject) => {
|
|
41
41
|
this.productImporter
|
|
42
|
-
.parseAndImport(job.data.fileContent, ctx, job.data.updateProductSlug, job.data.mainLanguage)
|
|
42
|
+
.parseAndImport(job.data.fileContent, ctx, job.data.updateProductSlug, job.data.mainLanguage, job.data.updatingStrategy)
|
|
43
43
|
.subscribe({
|
|
44
44
|
next: (result) => {
|
|
45
45
|
const processedCount = result.processed;
|
|
@@ -60,16 +60,17 @@ let ProductImportService = class ProductImportService {
|
|
|
60
60
|
},
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
-
async processFile(ctx, file, updateProductSlug, mainLanguage) {
|
|
63
|
+
async processFile(ctx, file, updateProductSlug, mainLanguage, updatingStrategy) {
|
|
64
64
|
const fileContent = file.buffer.toString('utf-8');
|
|
65
|
-
return this.triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage);
|
|
65
|
+
return this.triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage, updatingStrategy);
|
|
66
66
|
}
|
|
67
|
-
triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage) {
|
|
67
|
+
triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage, updatingStrategy) {
|
|
68
68
|
return this.productImportQueue.add({
|
|
69
69
|
ctx: ctx.serialize(),
|
|
70
70
|
fileContent,
|
|
71
71
|
updateProductSlug,
|
|
72
72
|
mainLanguage,
|
|
73
|
+
updatingStrategy,
|
|
73
74
|
});
|
|
74
75
|
}
|
|
75
76
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -37,4 +37,5 @@ export type JsonAsset = {
|
|
|
37
37
|
type ExportFields = Array<ProductFields | VariantFields>;
|
|
38
38
|
type ProductFields = 'productId' | 'name' | 'slug' | 'description' | 'assets' | 'facets' | 'optionGroups';
|
|
39
39
|
type VariantFields = 'sku' | 'optionValues' | 'price' | 'taxCategory' | 'stockOnHand' | 'trackInventory' | 'variantAssets' | 'variantFacets';
|
|
40
|
+
export type UpdatingStrategy = 'replace' | 'merge';
|
|
40
41
|
export {};
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
<div class="input-row" style="margin-top: 16px">
|
|
15
15
|
<label style="margin-bottom: 8px; display: inline-block">{{
|
|
16
|
-
'product-export.dialog.
|
|
16
|
+
'product-export.dialog.availableExportFieldsLabel' | translate
|
|
17
17
|
}}</label>
|
|
18
18
|
<div *ngFor="let field of availableExportFields">
|
|
19
19
|
<input
|
|
@@ -3,6 +3,7 @@ import { NotificationService, LocalStorageService } from '@vendure/admin-ui/core
|
|
|
3
3
|
import { TranslateService } from '@ngx-translate/core';
|
|
4
4
|
import { AdminUiConfig } from '@vendure/common/lib/shared-types';
|
|
5
5
|
import { Channel, LanguageCode } from '@vendure/core';
|
|
6
|
+
type UpdatingStrategy = 'replace' | 'merge';
|
|
6
7
|
type ImportOptions = {
|
|
7
8
|
updateProductSlug?: string;
|
|
8
9
|
};
|
|
@@ -26,6 +27,7 @@ export declare class ProductImportComponent implements OnInit {
|
|
|
26
27
|
updateProductSlug: boolean;
|
|
27
28
|
selectedMainLanguage: LanguageCode | undefined;
|
|
28
29
|
availableLanguages: LanguageCode[];
|
|
30
|
+
updatingStrategy: UpdatingStrategy;
|
|
29
31
|
fileUploadElement: ElementRef<HTMLInputElement>;
|
|
30
32
|
config: PluginInitOptions;
|
|
31
33
|
appConfig: AdminUiConfig;
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
</div>
|
|
26
26
|
</vdr-form-field>
|
|
27
27
|
</div>
|
|
28
|
-
<div
|
|
28
|
+
<div
|
|
29
|
+
class="form-grid"
|
|
30
|
+
*ngIf="selectedFile"
|
|
31
|
+
style="margin-top: 24px; border-top: 1px solid #cdcdcd"
|
|
32
|
+
>
|
|
29
33
|
<div
|
|
30
34
|
class="input-row"
|
|
31
35
|
style="margin-top: 16px"
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
</div>
|
|
45
49
|
|
|
46
50
|
<div class="input-row" style="margin-top: 16px">
|
|
47
|
-
<div>
|
|
51
|
+
<div>{{ 'product-import.card.select-main-language' | translate }}</div>
|
|
48
52
|
<div *ngFor="let lang of availableLanguages">
|
|
49
53
|
<label>
|
|
50
54
|
<input
|
|
@@ -58,6 +62,49 @@
|
|
|
58
62
|
{{ lang }}
|
|
59
63
|
</label>
|
|
60
64
|
</div>
|
|
65
|
+
<div
|
|
66
|
+
style="
|
|
67
|
+
margin-top: 8px;
|
|
68
|
+
padding-top: 8px;
|
|
69
|
+
font-style: italic;
|
|
70
|
+
font-size: 12px;
|
|
71
|
+
line-height: 1.4;
|
|
72
|
+
"
|
|
73
|
+
>
|
|
74
|
+
<clr-icon shape="info-circle" size="16" class="is-info"></clr-icon>
|
|
75
|
+
{{ 'product-import.card.main-language-info' | translate }}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div class="input-row" style="margin-top: 16px">
|
|
80
|
+
<div>{{ 'product-import.card.updating-strategy' | translate }}</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label>
|
|
83
|
+
<input
|
|
84
|
+
type="radio"
|
|
85
|
+
name="updatingStrategy"
|
|
86
|
+
value="merge"
|
|
87
|
+
[(ngModel)]="updatingStrategy"
|
|
88
|
+
style="width: fit-content"
|
|
89
|
+
/>
|
|
90
|
+
{{ 'product-import.card.merge' | translate }}
|
|
91
|
+
</label>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<label>
|
|
95
|
+
<input
|
|
96
|
+
type="radio"
|
|
97
|
+
name="updatingStrategy"
|
|
98
|
+
value="replace"
|
|
99
|
+
[(ngModel)]="updatingStrategy"
|
|
100
|
+
style="width: fit-content"
|
|
101
|
+
/>
|
|
102
|
+
{{ 'product-import.card.overwrite' | translate }}<br />
|
|
103
|
+
<span *ngIf="updatingStrategy === 'replace'" style="color: red; font-size: 12px">{{
|
|
104
|
+
'product-import.card.overwrite-warning' | translate
|
|
105
|
+
}}</span>
|
|
106
|
+
</label>
|
|
107
|
+
</div>
|
|
61
108
|
</div>
|
|
62
109
|
</div>
|
|
63
110
|
</vdr-card>
|
|
@@ -26,6 +26,7 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
26
26
|
updateProductSlug = true;
|
|
27
27
|
selectedMainLanguage;
|
|
28
28
|
availableLanguages = [];
|
|
29
|
+
updatingStrategy = 'merge';
|
|
29
30
|
fileUploadElement;
|
|
30
31
|
config;
|
|
31
32
|
appConfig;
|
|
@@ -90,6 +91,7 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
90
91
|
this.selectedFile = null;
|
|
91
92
|
this.validateFile = null;
|
|
92
93
|
this.selectedMainLanguage = undefined;
|
|
94
|
+
this.updatingStrategy = 'merge';
|
|
93
95
|
}
|
|
94
96
|
async validateCsvStructure(file) {
|
|
95
97
|
const baseColumns = ['sku'];
|
|
@@ -121,6 +123,15 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
121
123
|
lines.splice(index, 1);
|
|
122
124
|
}
|
|
123
125
|
});
|
|
126
|
+
// Check if number of columns for each line is consistent and the same as headers
|
|
127
|
+
const headerLength = this.splitLines(lines[0]).length;
|
|
128
|
+
const inconsistentLines = lines.filter((line) => this.splitLines(line).length !== headerLength);
|
|
129
|
+
if (inconsistentLines.length > 0) {
|
|
130
|
+
console.log('Inconsistent number of columns in the CSV file.');
|
|
131
|
+
this.notificationService.error(this.translate.instant('product-import.notifications.inconsistent-columns'));
|
|
132
|
+
resolve({ isValid: false });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
124
135
|
if (lines.length > 0) {
|
|
125
136
|
const header = this.splitLines(lines[0]).map((col) => col.trim());
|
|
126
137
|
console.log('CSV Header:', header);
|
|
@@ -282,11 +293,12 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
282
293
|
});
|
|
283
294
|
}
|
|
284
295
|
async uploadFile() {
|
|
285
|
-
if (this.selectedFile && this.selectedMainLanguage) {
|
|
296
|
+
if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
|
|
286
297
|
const formData = new FormData();
|
|
287
298
|
formData.append('file', this.selectedFile);
|
|
288
299
|
formData.append('updateProductSlug', this.updateProductSlug.toString());
|
|
289
300
|
formData.append('mainLanguage', this.selectedMainLanguage);
|
|
301
|
+
formData.append('updatingStrategy', this.updatingStrategy);
|
|
290
302
|
try {
|
|
291
303
|
const res = await fetch(`${this.serverPath}/product-import/upload`, {
|
|
292
304
|
method: 'POST',
|
|
@@ -7,6 +7,8 @@ import { endsWith, startsWith, size, uniq } from 'lodash'
|
|
|
7
7
|
import { AdminUiConfig } from '@vendure/common/lib/shared-types'
|
|
8
8
|
import { Channel, LanguageCode } from '@vendure/core'
|
|
9
9
|
|
|
10
|
+
type UpdatingStrategy = 'replace' | 'merge'
|
|
11
|
+
|
|
10
12
|
type ImportOptions = {
|
|
11
13
|
updateProductSlug?: string
|
|
12
14
|
}
|
|
@@ -34,6 +36,7 @@ export class ProductImportComponent implements OnInit {
|
|
|
34
36
|
updateProductSlug = true
|
|
35
37
|
selectedMainLanguage: LanguageCode | undefined
|
|
36
38
|
availableLanguages: LanguageCode[] = []
|
|
39
|
+
updatingStrategy: UpdatingStrategy = 'merge'
|
|
37
40
|
|
|
38
41
|
@ViewChild('fileUpload', { static: false }) fileUploadElement: ElementRef<HTMLInputElement>
|
|
39
42
|
|
|
@@ -114,6 +117,7 @@ export class ProductImportComponent implements OnInit {
|
|
|
114
117
|
this.selectedFile = null
|
|
115
118
|
this.validateFile = null
|
|
116
119
|
this.selectedMainLanguage = undefined
|
|
120
|
+
this.updatingStrategy = 'merge'
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
async validateCsvStructure(file: File): Promise<ValidateReturnType> {
|
|
@@ -151,6 +155,22 @@ export class ProductImportComponent implements OnInit {
|
|
|
151
155
|
lines.splice(index, 1)
|
|
152
156
|
}
|
|
153
157
|
})
|
|
158
|
+
|
|
159
|
+
// Check if number of columns for each line is consistent and the same as headers
|
|
160
|
+
const headerLength = this.splitLines(lines[0]).length
|
|
161
|
+
const inconsistentLines = lines.filter(
|
|
162
|
+
(line) => this.splitLines(line).length !== headerLength,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (inconsistentLines.length > 0) {
|
|
166
|
+
console.log('Inconsistent number of columns in the CSV file.')
|
|
167
|
+
this.notificationService.error(
|
|
168
|
+
this.translate.instant('product-import.notifications.inconsistent-columns'),
|
|
169
|
+
)
|
|
170
|
+
resolve({ isValid: false })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
154
174
|
if (lines.length > 0) {
|
|
155
175
|
const header = this.splitLines(lines[0]).map((col) => col.trim())
|
|
156
176
|
console.log('CSV Header:', header)
|
|
@@ -398,11 +418,12 @@ export class ProductImportComponent implements OnInit {
|
|
|
398
418
|
}
|
|
399
419
|
|
|
400
420
|
async uploadFile(): Promise<void> {
|
|
401
|
-
if (this.selectedFile && this.selectedMainLanguage) {
|
|
421
|
+
if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
|
|
402
422
|
const formData = new FormData()
|
|
403
423
|
formData.append('file', this.selectedFile)
|
|
404
424
|
formData.append('updateProductSlug', this.updateProductSlug.toString())
|
|
405
425
|
formData.append('mainLanguage', this.selectedMainLanguage)
|
|
426
|
+
formData.append('updatingStrategy', this.updatingStrategy)
|
|
406
427
|
|
|
407
428
|
try {
|
|
408
429
|
const res = await fetch(`${this.serverPath}/product-import/upload`, {
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
"description": "Choose a CSV file to import products from",
|
|
7
7
|
"button-select": "Select file",
|
|
8
8
|
"button-upload": "Upload file",
|
|
9
|
-
"update-product-slugs": "Update product slugs based on name"
|
|
9
|
+
"update-product-slugs": "Update product slugs based on name",
|
|
10
|
+
"select-main-language": "Select main language: ",
|
|
11
|
+
"updating-strategy": "Updating strategy for facets and assets: ",
|
|
12
|
+
"overwrite": "Overwrite",
|
|
13
|
+
"overwrite-warning": "Warning: This will remove existing facets and assets from products and variants.",
|
|
14
|
+
"merge": "Merge",
|
|
15
|
+
"main-language-info": "The main language is used as a fallback when translations are missing. It is recommended to select the language that is used the most in your system. The main language will be used to create products, the other languages will be used to fill in translations."
|
|
10
16
|
},
|
|
11
17
|
"notifications": {
|
|
12
18
|
"invalid-file-type": "Invalid file type. Please upload a CSV file.",
|
|
@@ -23,7 +29,8 @@
|
|
|
23
29
|
"missing-languages": "Your CSV file contains languages that are not available in the system. Please add the languages to the system or remove the languages that are not used and try again.",
|
|
24
30
|
"mismatched-option-values": "Some product variants are missing product options. Please fill in all options: ",
|
|
25
31
|
"missing-main-language-name": "Main language is missing for name. Please add the main language and try again.",
|
|
26
|
-
"missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported: "
|
|
32
|
+
"missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported: ",
|
|
33
|
+
"inconsistent-columns": "The columns in the file are inconsistent. Please ensure all rows have the same number of columns."
|
|
27
34
|
}
|
|
28
35
|
},
|
|
29
36
|
"product-export": {
|
|
@@ -38,7 +45,8 @@
|
|
|
38
45
|
"exportAssetsAsOptions": {
|
|
39
46
|
"url": "URL",
|
|
40
47
|
"json": "JSON"
|
|
41
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"availableExportFieldsLabel": "Fields to export"
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
"description": "Välj en CSV-fil att importera produkter från",
|
|
7
7
|
"button-select": "Välj fil",
|
|
8
8
|
"button-upload": "Ladda upp fil",
|
|
9
|
-
"update-product-slugs": "Uppdatera produkt-sluggar utifrån namn"
|
|
9
|
+
"update-product-slugs": "Uppdatera produkt-sluggar utifrån namn",
|
|
10
|
+
"select-main-language": "Välj huvudspråk: ",
|
|
11
|
+
"updating-strategy": "Uppdateringsstrategi för etiketter och filer: ",
|
|
12
|
+
"overwrite": "Skriv över",
|
|
13
|
+
"overwrite-warning": "Varning: Detta kommer ta bort befintliga etiketter och filer från produkter och varianter.",
|
|
14
|
+
"merge": "Sammanfoga",
|
|
15
|
+
"main-language-info": "Huvudspråket används som fallback när översättningar saknas. Det rekommenderas att välja det språk som används mest i din kanal. Huvudspråket kommer användas för att skapa upp produkter, de övriga språken kommer användas för att fylla i översättningar."
|
|
10
16
|
},
|
|
11
17
|
"notifications": {
|
|
12
18
|
"invalid-file-type": "Felaktig filtyp. Vänligen ladda upp en CSV-fil.",
|
|
@@ -23,7 +29,8 @@
|
|
|
23
29
|
"missing-languages": "Din CSV-fil innehåller språk som inte är tillgängliga i den här kanalen. Vänligen lägg till språken i kanalen eller ta bort de språk som inte används och försök igen.",
|
|
24
30
|
"mismatched-option-values": "Antalet produktalternativ för vissa produktvarianter är felaktigt: ",
|
|
25
31
|
"missing-main-language-name": "Huvudspråket saknas för namn. Vänligen lägg till huvudspråket och försök igen.",
|
|
26
|
-
"missing-translation-values": "Det saknas översättningar för vissa fält för huvudspråket som existerar i andra språk. Dessa kommer inte importeras: "
|
|
32
|
+
"missing-translation-values": "Det saknas översättningar för vissa fält för huvudspråket som existerar i andra språk. Dessa kommer inte importeras: ",
|
|
33
|
+
"inconsistent-columns": "Kolumnerna i filen är inkonsekventa. Vänligen kontrollera att alla rader har samma antal kolumner."
|
|
27
34
|
}
|
|
28
35
|
},
|
|
29
36
|
"product-export": {
|
|
@@ -38,7 +45,8 @@
|
|
|
38
45
|
"exportAssetsAsOptions": {
|
|
39
46
|
"url": "URL",
|
|
40
47
|
"json": "JSON"
|
|
41
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"availableExportFieldsLabel": "Fält att exportera"
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
}
|
package/package.json
CHANGED