@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.
@@ -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.Res)()),
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
- p.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
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
- facet.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
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
- variant.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
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 variantPrice = new core_1.ProductVariantPrice({
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
- .update({ id: updatedVariant.id }, variantPrice);
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.defaultChannel.defaultCurrencyCode,
418
+ currencyCode: this.importCtx.channel.defaultCurrencyCode,
396
419
  });
397
420
  variantPrice.variant = createdVariant;
398
- await this.connection
399
- .getRepository(this.importCtx, core_1.ProductVariantPrice)
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.customFieldsLabel' | translate
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 class="form-grid" *ngIf="selectedFile">
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>Välj huvudspråk för filen:</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/product-import-export-plugin",
3
- "version": "2.2.0-dev.0",
3
+ "version": "2.2.0-dev.1",
4
4
  "description": "A Vendure plugin for importing and exporting products using a CSV file",
5
5
  "author": "Haus Tech",
6
6
  "repository": {