@haus-tech/product-import-export-plugin 2.2.0-dev.0 → 2.2.0-dev.1
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/product-importer.d.ts +3 -3
- package/dist/providers/import-providers/product-importer.js +5 -5
- package/dist/services/extended-fast-importer.service.d.ts +3 -2
- package/dist/services/extended-fast-importer.service.js +45 -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 +46 -2
- package/dist/ui/components/product-import.component.js +12 -1
- package/dist/ui/components/product-import.component.ts +21 -1
- package/dist/ui/translations/en.json +10 -3
- package/dist/ui/translations/sv.json +10 -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([
|
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Channel, ChannelService, ID, RequestContextService, StockMovementService, TransactionalConnection, TranslatableSaver } from '@vendure/core';
|
|
1
|
+
import { Channel, ChannelService, EntityHydrator, ID, 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
|
|
@@ -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,8 @@ 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');
|
|
63
66
|
if (input.featuredAssetId) {
|
|
64
67
|
p.featuredAsset = { id: input.featuredAssetId };
|
|
65
68
|
}
|
|
@@ -197,8 +200,9 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
197
200
|
input: { ...input, id: facetExists.id },
|
|
198
201
|
entityType: core_1.Facet,
|
|
199
202
|
translationType: core_1.FacetTranslation,
|
|
200
|
-
beforeSave: (facet) => {
|
|
201
|
-
|
|
203
|
+
beforeSave: async (facet) => {
|
|
204
|
+
await this.entityHydrator.hydrate(this.importCtx, facet, { relations: ['channels'] });
|
|
205
|
+
facet.channels = (0, unique_1.unique)([...facet.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
202
206
|
},
|
|
203
207
|
});
|
|
204
208
|
return facet.id;
|
|
@@ -208,7 +212,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
208
212
|
input,
|
|
209
213
|
entityType: core_1.Facet,
|
|
210
214
|
translationType: core_1.FacetTranslation,
|
|
211
|
-
beforeSave: (facet) => {
|
|
215
|
+
beforeSave: async (facet) => {
|
|
212
216
|
facet.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
213
217
|
},
|
|
214
218
|
});
|
|
@@ -228,9 +232,10 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
228
232
|
input: { ...input, id: facetValueExists.id },
|
|
229
233
|
entityType: core_1.FacetValue,
|
|
230
234
|
translationType: core_1.FacetValueTranslation,
|
|
231
|
-
beforeSave: (facetValue) => {
|
|
235
|
+
beforeSave: async (facetValue) => {
|
|
236
|
+
await this.entityHydrator.hydrate(this.importCtx, facetValue, { relations: ['channels'] });
|
|
232
237
|
facetValue.facet = { id: input.facetId };
|
|
233
|
-
facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
238
|
+
facetValue.channels = (0, unique_1.unique)([...facetValue.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
234
239
|
},
|
|
235
240
|
});
|
|
236
241
|
return updatedFacetValue.id;
|
|
@@ -240,7 +245,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
240
245
|
input,
|
|
241
246
|
entityType: core_1.FacetValue,
|
|
242
247
|
translationType: core_1.FacetValueTranslation,
|
|
243
|
-
beforeSave: (facetValue) => {
|
|
248
|
+
beforeSave: async (facetValue) => {
|
|
244
249
|
facetValue.facet = { id: input.facetId };
|
|
245
250
|
facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
|
|
246
251
|
},
|
|
@@ -299,7 +304,8 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
299
304
|
entityType: core_1.ProductVariant,
|
|
300
305
|
translationType: core_1.ProductVariantTranslation,
|
|
301
306
|
beforeSave: async (variant) => {
|
|
302
|
-
|
|
307
|
+
await this.entityHydrator.hydrate(this.importCtx, variant, { relations: ['channels'] });
|
|
308
|
+
variant.channels = (0, unique_1.unique)([...variant.channels, this.defaultChannel, this.importCtx.channel], 'id');
|
|
303
309
|
const { optionIds } = input;
|
|
304
310
|
if (optionIds && optionIds.length) {
|
|
305
311
|
variant.options = optionIds.map((id) => ({ id }));
|
|
@@ -331,15 +337,32 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
331
337
|
await this.stockMovementService.adjustProductVariantStock(this.importCtx, updatedVariant.id, input.stockOnHand ?? 0);
|
|
332
338
|
const assignedChannelIds = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id').map((c) => c.id);
|
|
333
339
|
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
|
|
340
|
+
const allPrices = await this.connection
|
|
341
341
|
.getRepository(this.importCtx, core_1.ProductVariantPrice)
|
|
342
|
-
.
|
|
342
|
+
.find({
|
|
343
|
+
where: {
|
|
344
|
+
variant: { id: updatedVariant.id },
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
let existingVariantPrice = allPrices.find((p) => (0, core_1.idsAreEqual)(p.channelId, channelId) &&
|
|
348
|
+
p.currencyCode === this.importCtx.channel.defaultCurrencyCode);
|
|
349
|
+
if (!existingVariantPrice) {
|
|
350
|
+
const variantPrice = new core_1.ProductVariantPrice({
|
|
351
|
+
price: input.price,
|
|
352
|
+
channelId,
|
|
353
|
+
currencyCode: this.importCtx.channel.defaultCurrencyCode,
|
|
354
|
+
});
|
|
355
|
+
variantPrice.variant = updatedVariant;
|
|
356
|
+
await this.connection.rawConnection
|
|
357
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
358
|
+
.save(variantPrice, { reload: false });
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
existingVariantPrice.price = input.price;
|
|
362
|
+
await this.connection.rawConnection
|
|
363
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
364
|
+
.save(existingVariantPrice, { reload: false });
|
|
365
|
+
}
|
|
343
366
|
}
|
|
344
367
|
return updatedVariant.id;
|
|
345
368
|
}
|
|
@@ -392,11 +415,11 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
392
415
|
const variantPrice = new core_1.ProductVariantPrice({
|
|
393
416
|
price: input.price,
|
|
394
417
|
channelId,
|
|
395
|
-
currencyCode: this.
|
|
418
|
+
currencyCode: this.importCtx.channel.defaultCurrencyCode,
|
|
396
419
|
});
|
|
397
420
|
variantPrice.variant = createdVariant;
|
|
398
|
-
await this.connection
|
|
399
|
-
.getRepository(
|
|
421
|
+
await this.connection.rawConnection
|
|
422
|
+
.getRepository(core_1.ProductVariantPrice)
|
|
400
423
|
.save(variantPrice, { reload: false });
|
|
401
424
|
}
|
|
402
425
|
return createdVariant.id;
|
|
@@ -413,6 +436,7 @@ ExtendedFastImporterService = __decorate([
|
|
|
413
436
|
core_1.ChannelService,
|
|
414
437
|
core_1.StockMovementService,
|
|
415
438
|
core_1.TranslatableSaver,
|
|
416
|
-
core_1.RequestContextService
|
|
439
|
+
core_1.RequestContextService,
|
|
440
|
+
core_1.EntityHydrator])
|
|
417
441
|
], ExtendedFastImporterService);
|
|
418
442
|
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 = 'overwrite' | '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 = 'overwrite' | '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,46 @@
|
|
|
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="overwrite"
|
|
87
|
+
[(ngModel)]="updatingStrategy"
|
|
88
|
+
style="width: fit-content"
|
|
89
|
+
/>
|
|
90
|
+
{{ 'product-import.card.overwrite' | translate }}
|
|
91
|
+
</label>
|
|
92
|
+
</div>
|
|
93
|
+
<div>
|
|
94
|
+
<label>
|
|
95
|
+
<input
|
|
96
|
+
type="radio"
|
|
97
|
+
name="updatingStrategy"
|
|
98
|
+
value="merge"
|
|
99
|
+
[(ngModel)]="updatingStrategy"
|
|
100
|
+
style="width: fit-content"
|
|
101
|
+
/>
|
|
102
|
+
{{ 'product-import.card.merge' | translate }}
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
61
105
|
</div>
|
|
62
106
|
</div>
|
|
63
107
|
</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;
|
|
@@ -121,6 +122,15 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
121
122
|
lines.splice(index, 1);
|
|
122
123
|
}
|
|
123
124
|
});
|
|
125
|
+
// Check if number of columns for each line is consistent and the same as headers
|
|
126
|
+
const headerLength = this.splitLines(lines[0]).length;
|
|
127
|
+
const inconsistentLines = lines.filter((line) => this.splitLines(line).length !== headerLength);
|
|
128
|
+
if (inconsistentLines.length > 0) {
|
|
129
|
+
console.log('Inconsistent number of columns in the CSV file.');
|
|
130
|
+
this.notificationService.error(this.translate.instant('product-import.notifications.inconsistent-columns'));
|
|
131
|
+
resolve({ isValid: false });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
124
134
|
if (lines.length > 0) {
|
|
125
135
|
const header = this.splitLines(lines[0]).map((col) => col.trim());
|
|
126
136
|
console.log('CSV Header:', header);
|
|
@@ -282,11 +292,12 @@ let ProductImportComponent = class ProductImportComponent {
|
|
|
282
292
|
});
|
|
283
293
|
}
|
|
284
294
|
async uploadFile() {
|
|
285
|
-
if (this.selectedFile && this.selectedMainLanguage) {
|
|
295
|
+
if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
|
|
286
296
|
const formData = new FormData();
|
|
287
297
|
formData.append('file', this.selectedFile);
|
|
288
298
|
formData.append('updateProductSlug', this.updateProductSlug.toString());
|
|
289
299
|
formData.append('mainLanguage', this.selectedMainLanguage);
|
|
300
|
+
formData.append('updatingStrategy', this.updatingStrategy);
|
|
290
301
|
try {
|
|
291
302
|
const res = await fetch(`${this.serverPath}/product-import/upload`, {
|
|
292
303
|
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 = 'overwrite' | '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
|
|
|
@@ -151,6 +154,22 @@ export class ProductImportComponent implements OnInit {
|
|
|
151
154
|
lines.splice(index, 1)
|
|
152
155
|
}
|
|
153
156
|
})
|
|
157
|
+
|
|
158
|
+
// Check if number of columns for each line is consistent and the same as headers
|
|
159
|
+
const headerLength = this.splitLines(lines[0]).length
|
|
160
|
+
const inconsistentLines = lines.filter(
|
|
161
|
+
(line) => this.splitLines(line).length !== headerLength,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if (inconsistentLines.length > 0) {
|
|
165
|
+
console.log('Inconsistent number of columns in the CSV file.')
|
|
166
|
+
this.notificationService.error(
|
|
167
|
+
this.translate.instant('product-import.notifications.inconsistent-columns'),
|
|
168
|
+
)
|
|
169
|
+
resolve({ isValid: false })
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
if (lines.length > 0) {
|
|
155
174
|
const header = this.splitLines(lines[0]).map((col) => col.trim())
|
|
156
175
|
console.log('CSV Header:', header)
|
|
@@ -398,11 +417,12 @@ export class ProductImportComponent implements OnInit {
|
|
|
398
417
|
}
|
|
399
418
|
|
|
400
419
|
async uploadFile(): Promise<void> {
|
|
401
|
-
if (this.selectedFile && this.selectedMainLanguage) {
|
|
420
|
+
if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
|
|
402
421
|
const formData = new FormData()
|
|
403
422
|
formData.append('file', this.selectedFile)
|
|
404
423
|
formData.append('updateProductSlug', this.updateProductSlug.toString())
|
|
405
424
|
formData.append('mainLanguage', this.selectedMainLanguage)
|
|
425
|
+
formData.append('updatingStrategy', this.updatingStrategy)
|
|
406
426
|
|
|
407
427
|
try {
|
|
408
428
|
const res = await fetch(`${this.serverPath}/product-import/upload`, {
|
|
@@ -6,7 +6,12 @@
|
|
|
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
|
+
"merge": "Merge",
|
|
14
|
+
"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
15
|
},
|
|
11
16
|
"notifications": {
|
|
12
17
|
"invalid-file-type": "Invalid file type. Please upload a CSV file.",
|
|
@@ -23,7 +28,8 @@
|
|
|
23
28
|
"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
29
|
"mismatched-option-values": "Some product variants are missing product options. Please fill in all options: ",
|
|
25
30
|
"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: "
|
|
31
|
+
"missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported: ",
|
|
32
|
+
"inconsistent-columns": "The columns in the file are inconsistent. Please ensure all rows have the same number of columns."
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
"product-export": {
|
|
@@ -38,7 +44,8 @@
|
|
|
38
44
|
"exportAssetsAsOptions": {
|
|
39
45
|
"url": "URL",
|
|
40
46
|
"json": "JSON"
|
|
41
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"availableExportFieldsLabel": "Fields to export"
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
@@ -6,7 +6,12 @@
|
|
|
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
|
+
"merge": "Sammanfoga",
|
|
14
|
+
"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
15
|
},
|
|
11
16
|
"notifications": {
|
|
12
17
|
"invalid-file-type": "Felaktig filtyp. Vänligen ladda upp en CSV-fil.",
|
|
@@ -23,7 +28,8 @@
|
|
|
23
28
|
"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
29
|
"mismatched-option-values": "Antalet produktalternativ för vissa produktvarianter är felaktigt: ",
|
|
25
30
|
"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: "
|
|
31
|
+
"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
|
+
"inconsistent-columns": "Kolumnerna i filen är inkonsekventa. Vänligen kontrollera att alla rader har samma antal kolumner."
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
"product-export": {
|
|
@@ -38,7 +44,8 @@
|
|
|
38
44
|
"exportAssetsAsOptions": {
|
|
39
45
|
"url": "URL",
|
|
40
46
|
"json": "JSON"
|
|
41
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"availableExportFieldsLabel": "Fält att exportera"
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
}
|
package/package.json
CHANGED