@haus-tech/product-import-export-plugin 2.2.0-dev.0
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/plugin.controller.d.ts +8 -0
- package/dist/api/plugin.controller.js +49 -0
- package/dist/api/product-export.controller.d.ts +14 -0
- package/dist/api/product-export.controller.js +117 -0
- package/dist/api/product-import.controller.d.ts +11 -0
- package/dist/api/product-import.controller.js +62 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/helpers/generate-asset-hash.d.ts +2 -0
- package/dist/helpers/generate-asset-hash.js +15 -0
- package/dist/helpers/normalize-string.d.ts +1 -0
- package/dist/helpers/normalize-string.js +16 -0
- package/dist/helpers/remove-update-at-fields.d.ts +5 -0
- package/dist/helpers/remove-update-at-fields.js +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/dist/product-import-export.plugin.d.ts +13 -0
- package/dist/product-import-export.plugin.js +153 -0
- package/dist/providers/import-providers/asset-importer.d.ts +27 -0
- package/dist/providers/import-providers/asset-importer.js +127 -0
- package/dist/providers/import-providers/import-parser.d.ts +137 -0
- package/dist/providers/import-providers/import-parser.js +600 -0
- package/dist/providers/import-providers/product-importer.d.ts +47 -0
- package/dist/providers/import-providers/product-importer.js +729 -0
- package/dist/services/extended-fast-importer.service.d.ts +41 -0
- package/dist/services/extended-fast-importer.service.js +418 -0
- package/dist/services/product-export.service.d.ts +23 -0
- package/dist/services/product-export.service.js +340 -0
- package/dist/services/product-import.service.d.ts +21 -0
- package/dist/services/product-import.service.js +83 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +2 -0
- package/dist/ui/components/export-dialog.component.d.ts +43 -0
- package/dist/ui/components/export-dialog.component.html +78 -0
- package/dist/ui/components/export-dialog.component.js +108 -0
- package/dist/ui/components/export-dialog.component.ts +142 -0
- package/dist/ui/components/product-import.component.d.ts +55 -0
- package/dist/ui/components/product-import.component.html +76 -0
- package/dist/ui/components/product-import.component.js +398 -0
- package/dist/ui/components/product-import.component.ts +528 -0
- package/dist/ui/product-export.service.d.ts +14 -0
- package/dist/ui/product-export.service.js +107 -0
- package/dist/ui/product-export.service.ts +107 -0
- package/dist/ui/providers.d.ts +2 -0
- package/dist/ui/providers.js +49 -0
- package/dist/ui/providers.ts +57 -0
- package/dist/ui/routes.d.ts +29 -0
- package/dist/ui/routes.js +12 -0
- package/dist/ui/routes.ts +11 -0
- package/dist/ui/translations/en.json +44 -0
- package/dist/ui/translations/sv.json +44 -0
- package/package.json +30 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
35
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
36
|
+
};
|
|
37
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
38
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.ProductExportService = void 0;
|
|
42
|
+
const common_1 = require("@nestjs/common");
|
|
43
|
+
const core_1 = require("@vendure/core");
|
|
44
|
+
const constants_1 = require("../constants");
|
|
45
|
+
const csv_writer_1 = require("csv-writer");
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os_1 = __importDefault(require("os"));
|
|
48
|
+
const lodash_1 = require("lodash");
|
|
49
|
+
let ProductExportService = class ProductExportService {
|
|
50
|
+
options;
|
|
51
|
+
productService;
|
|
52
|
+
stockLevelService;
|
|
53
|
+
entityHydratorService;
|
|
54
|
+
channelService;
|
|
55
|
+
configService;
|
|
56
|
+
constructor(options, productService, stockLevelService, entityHydratorService, channelService, configService) {
|
|
57
|
+
this.options = options;
|
|
58
|
+
this.productService = productService;
|
|
59
|
+
this.stockLevelService = stockLevelService;
|
|
60
|
+
this.entityHydratorService = entityHydratorService;
|
|
61
|
+
this.channelService = channelService;
|
|
62
|
+
this.configService = configService;
|
|
63
|
+
}
|
|
64
|
+
async createExportFile(ctx, selectionIds, fileName, selectedCustomFields, exportAssetsAs, selectedExportFields) {
|
|
65
|
+
const products = await this.productService.findByIds(ctx, selectionIds);
|
|
66
|
+
const channel = await this.channelService.findOne(ctx, ctx.channelId);
|
|
67
|
+
if (!channel) {
|
|
68
|
+
throw new Error('Channel not found');
|
|
69
|
+
}
|
|
70
|
+
const languages = channel.availableLanguageCodes;
|
|
71
|
+
const allCustomFieldNames = await this.getCustomFields(ctx, selectionIds);
|
|
72
|
+
const filteredCustomFieldNames = selectedCustomFields
|
|
73
|
+
.split(',')
|
|
74
|
+
.filter((field) => allCustomFieldNames.some((f) => f.name === field))
|
|
75
|
+
.map((field) => {
|
|
76
|
+
const found = allCustomFieldNames.find((f) => f.name === field);
|
|
77
|
+
return found ? `${found.name}:${found.type}` : '';
|
|
78
|
+
});
|
|
79
|
+
const exportFile = path.join(os_1.default.tmpdir(), fileName);
|
|
80
|
+
const headers = [];
|
|
81
|
+
headers.push({ id: 'productId', title: 'productId' });
|
|
82
|
+
// Add headers for translations
|
|
83
|
+
for (const lang of languages) {
|
|
84
|
+
headers.push({ id: `name:${lang}`, title: `name:${lang}` });
|
|
85
|
+
headers.push({ id: `slug:${lang}`, title: `slug:${lang}` });
|
|
86
|
+
headers.push({ id: `description:${lang}`, title: `description:${lang}` });
|
|
87
|
+
}
|
|
88
|
+
headers.push({ id: 'assets', title: 'assets' }, ...languages.map((lang) => ({ id: `facets:${lang}`, title: `facets:${lang}` })), ...languages.map((lang) => ({ id: `optionGroups:${lang}`, title: `optionGroups:${lang}` })), ...languages.map((lang) => ({ id: `optionValues:${lang}`, title: `optionValues:${lang}` })), { id: 'sku', title: 'sku' }, { id: 'price', title: 'price' }, { id: 'taxCategory', title: 'taxCategory' }, { id: 'stockOnHand', title: 'stockOnHand' }, { id: 'trackInventory', title: 'trackInventory' }, { id: 'variantAssets', title: 'variantAssets' }, ...languages.map((lang) => ({ id: `variantFacets:${lang}`, title: `variantFacets:${lang}` })), ...filteredCustomFieldNames.map((field) => ({
|
|
89
|
+
id: field,
|
|
90
|
+
title: field,
|
|
91
|
+
})));
|
|
92
|
+
const selectedExportFieldsArray = selectedExportFields.split(',');
|
|
93
|
+
const selectedExportFieldsSet = new Set(selectedExportFieldsArray);
|
|
94
|
+
const filteredHeaders = headers.filter((header) => {
|
|
95
|
+
if (header.id.includes(':')) {
|
|
96
|
+
const field = header.id.split(':')[0];
|
|
97
|
+
return selectedExportFieldsSet.has(field);
|
|
98
|
+
}
|
|
99
|
+
return selectedExportFieldsSet.has(header.id);
|
|
100
|
+
});
|
|
101
|
+
const csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
|
|
102
|
+
path: exportFile,
|
|
103
|
+
header: filteredHeaders,
|
|
104
|
+
});
|
|
105
|
+
for (const product of products) {
|
|
106
|
+
const records = [];
|
|
107
|
+
const hydratedProduct = await this.entityHydratorService.hydrate(ctx, product, {
|
|
108
|
+
relations: [
|
|
109
|
+
'variants',
|
|
110
|
+
'facetValues',
|
|
111
|
+
'facetValues.facet',
|
|
112
|
+
'optionGroups',
|
|
113
|
+
'assets',
|
|
114
|
+
'variants.assets',
|
|
115
|
+
'variants.facetValues',
|
|
116
|
+
'variants.facetValues.facet',
|
|
117
|
+
'variants.options',
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
const { assets = [], facetValues = [], optionGroups = [], variants = [], translations = [], customFields = {}, } = hydratedProduct;
|
|
121
|
+
const nameTranslations = this.mapTranslations(translations, 'name', languages);
|
|
122
|
+
const slugTranslations = this.mapTranslations(translations, 'slug', languages);
|
|
123
|
+
const descriptionTranslations = this.mapTranslations(translations, 'description', languages);
|
|
124
|
+
// Filter out all variants that are soft deleted
|
|
125
|
+
const activeVariants = variants.filter((v) => !v.deletedAt);
|
|
126
|
+
const productAssets = assets.length === 0
|
|
127
|
+
? ''
|
|
128
|
+
: assets.length > 0
|
|
129
|
+
? this.handleAssets(assets.map(({ asset }) => asset), exportAssetsAs)
|
|
130
|
+
: this.handleAssets([hydratedProduct.featuredAsset], exportAssetsAs);
|
|
131
|
+
const productFacets = languages.reduce((acc, lang) => {
|
|
132
|
+
acc[lang] = facetValues
|
|
133
|
+
.map((facetValue) => this.mapFacetTranslations(facetValue, lang))
|
|
134
|
+
.filter((facet) => facet)
|
|
135
|
+
.join('|');
|
|
136
|
+
return acc;
|
|
137
|
+
}, {});
|
|
138
|
+
const optionGroupNames = languages.reduce((acc, lang) => {
|
|
139
|
+
acc[lang] = (0, lodash_1.sortBy)(optionGroups, (g) => g.id)
|
|
140
|
+
.map((group) => this.mapTranslations(group.translations, 'name', [lang])[lang])
|
|
141
|
+
.filter((name) => name)
|
|
142
|
+
.join('|');
|
|
143
|
+
return acc;
|
|
144
|
+
}, {});
|
|
145
|
+
for (const variant of activeVariants) {
|
|
146
|
+
const variantValues = languages.reduce((acc, lang) => {
|
|
147
|
+
acc[lang] = ((0, lodash_1.sortBy)(variant.options, (o) => o.groupId) || [])
|
|
148
|
+
.map((option) => this.mapTranslations(option.translations, 'name', [lang])[lang])
|
|
149
|
+
.filter((name) => name)
|
|
150
|
+
.join('|');
|
|
151
|
+
return acc;
|
|
152
|
+
}, {});
|
|
153
|
+
const variantAssets = this.handleAssets(variant.assets.map(({ asset }) => asset), exportAssetsAs);
|
|
154
|
+
const variantFacets = languages.reduce((acc, lang) => {
|
|
155
|
+
acc[lang] = (variant.facetValues || [])
|
|
156
|
+
.map((facet) => this.mapFacetTranslations(facet, lang))
|
|
157
|
+
.filter((facet) => facet)
|
|
158
|
+
.join('|');
|
|
159
|
+
return acc;
|
|
160
|
+
}, {});
|
|
161
|
+
const stockOnHand = await this.getStockOnHand(ctx, variant.id); // Adjusted to fetch stock details
|
|
162
|
+
const variantTranslations = variant.translations;
|
|
163
|
+
const variantNameTranslations = this.mapTranslations(variantTranslations, 'name', languages);
|
|
164
|
+
const record = {};
|
|
165
|
+
const convertToHTML = (text) => {
|
|
166
|
+
return text
|
|
167
|
+
.replace(/\n/g, '<br>')
|
|
168
|
+
.replace(/\t/g, ' ')
|
|
169
|
+
.replace(/,\s*'/g, ", '");
|
|
170
|
+
};
|
|
171
|
+
for (const lang of languages) {
|
|
172
|
+
const escapedDescription = convertToHTML(descriptionTranslations[lang]);
|
|
173
|
+
const escapedName = nameTranslations[lang]
|
|
174
|
+
.replace(/"/g, "'")
|
|
175
|
+
.replace(/\s+/g, ' ')
|
|
176
|
+
.replace(/,\s*'/g, ", '")
|
|
177
|
+
.replace(/'/g, "''");
|
|
178
|
+
record.productId = records.length === 0 ? product.id : '';
|
|
179
|
+
record[`name:${lang}`] = records.length === 0 ? escapedName || '' : '';
|
|
180
|
+
record[`slug:${lang}`] = records.length === 0 ? slugTranslations[lang] || '' : '';
|
|
181
|
+
record[`description:${lang}`] = records.length === 0 ? escapedDescription || '' : '';
|
|
182
|
+
record[`facets:${lang}`] = records.length === 0 ? productFacets[lang] : '';
|
|
183
|
+
record[`optionGroups:${lang}`] = records.length === 0 ? optionGroupNames[lang] : '';
|
|
184
|
+
record[`optionValues:${lang}`] = variantValues[lang];
|
|
185
|
+
record[`variantFacets:${lang}`] = variantFacets[lang];
|
|
186
|
+
}
|
|
187
|
+
const productVariantPrices = variant.productVariantPrices.filter((price) => price.channelId === ctx.channelId);
|
|
188
|
+
record.assets = records.length === 0 ? productAssets : '';
|
|
189
|
+
record.sku = variant.sku;
|
|
190
|
+
record.price = productVariantPrices[0]?.price / 100; // Assuming the price is stored in cents
|
|
191
|
+
record.taxCategory = 'standard'; // Replace with actual tax category if available
|
|
192
|
+
record.stockOnHand = stockOnHand;
|
|
193
|
+
record.trackInventory = variant.trackInventory.toLowerCase();
|
|
194
|
+
record.variantAssets = variantAssets;
|
|
195
|
+
for (const field of filteredCustomFieldNames) {
|
|
196
|
+
const [entity, fieldName, type] = field.split(':');
|
|
197
|
+
if (entity === 'product') {
|
|
198
|
+
record[field] =
|
|
199
|
+
this.handleCustomFields(customFields, fieldName, type, exportAssetsAs) || '';
|
|
200
|
+
}
|
|
201
|
+
else if (entity === 'variant') {
|
|
202
|
+
record[field] =
|
|
203
|
+
this.handleCustomFields(variant.customFields, fieldName, type, exportAssetsAs) || '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
records.push(record);
|
|
207
|
+
}
|
|
208
|
+
await csvWriter.writeRecords(records);
|
|
209
|
+
}
|
|
210
|
+
return exportFile;
|
|
211
|
+
}
|
|
212
|
+
handleAssets(assets, exportAssetsAs) {
|
|
213
|
+
if (!assets.length) {
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
if (exportAssetsAs === 'url') {
|
|
217
|
+
return assets.map((asset) => asset.source).join('|');
|
|
218
|
+
}
|
|
219
|
+
return JSON.stringify(assets.length > 1
|
|
220
|
+
? assets.map((asset) => ({
|
|
221
|
+
id: asset.id,
|
|
222
|
+
name: asset.name,
|
|
223
|
+
url: asset.source,
|
|
224
|
+
}))
|
|
225
|
+
: {
|
|
226
|
+
id: assets[0].id,
|
|
227
|
+
name: assets[0].name,
|
|
228
|
+
url: assets[0].source,
|
|
229
|
+
})
|
|
230
|
+
.replace(/"/g, "'") // Replace double quotes with single quotes
|
|
231
|
+
.replace(/\s+/g, ' ') // Remove unnecessary whitespace and line breaks
|
|
232
|
+
.replace(/,\s*'/g, ", '"); // Ensure clean spacing after commas
|
|
233
|
+
}
|
|
234
|
+
handleCustomFields(customFields, fieldName, type, exportAssetsAs) {
|
|
235
|
+
const fieldValue = customFields[fieldName];
|
|
236
|
+
if (!fieldValue) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const relationsTypes = [
|
|
240
|
+
...this.configService.customFields.Product.map((field) => {
|
|
241
|
+
if (field.type === 'relation')
|
|
242
|
+
return field.entity.name.toLowerCase();
|
|
243
|
+
}),
|
|
244
|
+
...this.configService.customFields.ProductVariant.map((field) => {
|
|
245
|
+
if (field.type === 'relation')
|
|
246
|
+
return field.entity.name.toLowerCase();
|
|
247
|
+
}),
|
|
248
|
+
].filter((type) => type);
|
|
249
|
+
if (relationsTypes.includes(type)) {
|
|
250
|
+
if (type === 'asset') {
|
|
251
|
+
return (customFields[fieldName] = this.handleAssets([fieldValue], exportAssetsAs));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
return (customFields[fieldName] = fieldValue.id);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (typeof fieldValue === 'object') {
|
|
259
|
+
customFields[fieldName] = customFields[fieldName] = JSON.stringify(fieldValue)
|
|
260
|
+
.replace(/"/g, "'") // Replace double quotes with single quotes
|
|
261
|
+
.replace(/\s+/g, ' ') // Remove unnecessary whitespace and line breaks
|
|
262
|
+
.replace(/,\s*'/g, ", '"); // Ensure clean spacing after commas
|
|
263
|
+
}
|
|
264
|
+
else if ((0, lodash_1.startsWith)(fieldValue, '{') || (0, lodash_1.startsWith)(fieldValue, '[')) {
|
|
265
|
+
customFields[fieldName] = fieldValue.replace(/"/g, "'").replace(/\s+/g, ' ');
|
|
266
|
+
return customFields[fieldName];
|
|
267
|
+
}
|
|
268
|
+
return customFields[fieldName];
|
|
269
|
+
}
|
|
270
|
+
// Method to fetch stock on hand for a variant
|
|
271
|
+
async getStockOnHand(ctx, variantId) {
|
|
272
|
+
const stockLevel = await this.stockLevelService.getAvailableStock(ctx, variantId);
|
|
273
|
+
return stockLevel.stockOnHand;
|
|
274
|
+
}
|
|
275
|
+
// Method to map translations for a specific field
|
|
276
|
+
mapTranslations(translations, field, languages) {
|
|
277
|
+
const translationMap = {};
|
|
278
|
+
for (const lang of languages) {
|
|
279
|
+
const translation = translations.find((t) => t.languageCode === lang);
|
|
280
|
+
translationMap[lang] = translation ? translation[field] : '';
|
|
281
|
+
}
|
|
282
|
+
return translationMap;
|
|
283
|
+
}
|
|
284
|
+
// Method to map facet translations
|
|
285
|
+
mapFacetTranslations(facetValue, lang) {
|
|
286
|
+
const facetNameTranslations = this.mapTranslations(facetValue.facet.translations, 'name', [
|
|
287
|
+
lang,
|
|
288
|
+
]);
|
|
289
|
+
const facetValueTranslations = this.mapTranslations(facetValue.translations, 'name', [lang]);
|
|
290
|
+
if (!facetNameTranslations[lang] || !facetValueTranslations[lang]) {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
return `${facetNameTranslations[lang]}:${facetValueTranslations[lang]}`;
|
|
294
|
+
}
|
|
295
|
+
async getCustomFields(ctx, productIds) {
|
|
296
|
+
const customFields = new Set();
|
|
297
|
+
(0, lodash_1.forEach)(this.configService.customFields.Product, (field) => {
|
|
298
|
+
if (field.type === 'relation') {
|
|
299
|
+
const entity = field.entity.name;
|
|
300
|
+
customFields.add({
|
|
301
|
+
name: `product:${field.name}`,
|
|
302
|
+
type: entity.toLowerCase(),
|
|
303
|
+
});
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
customFields.add({
|
|
307
|
+
name: `product:${field.name}`,
|
|
308
|
+
type: field.type,
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
(0, lodash_1.forEach)(this.configService.customFields.ProductVariant, (field) => {
|
|
312
|
+
if (field.type === 'relation') {
|
|
313
|
+
const entity = field.entity.name;
|
|
314
|
+
customFields.add({
|
|
315
|
+
name: `variant:${field.name}`,
|
|
316
|
+
type: entity.toLowerCase(),
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
customFields.add({
|
|
321
|
+
name: `variant:${field.name}`,
|
|
322
|
+
type: field.type,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
return Array.from(customFields);
|
|
326
|
+
}
|
|
327
|
+
async getConfig() {
|
|
328
|
+
return this.options;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
ProductExportService = __decorate([
|
|
332
|
+
(0, common_1.Injectable)(),
|
|
333
|
+
__param(0, (0, common_1.Inject)(constants_1.PRODUCT_IMPORT_EXPORT_PLUGIN_OPTIONS)),
|
|
334
|
+
__metadata("design:paramtypes", [Object, core_1.ProductService,
|
|
335
|
+
core_1.StockLevelService,
|
|
336
|
+
core_1.EntityHydrator,
|
|
337
|
+
core_1.ChannelService,
|
|
338
|
+
core_1.ConfigService])
|
|
339
|
+
], ProductExportService);
|
|
340
|
+
exports.ProductExportService = ProductExportService;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/// <reference types="Multer" />
|
|
2
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
3
|
+
import { RequestContext, JobQueueService, SerializedRequestContext, SearchService, LanguageCode } from '@vendure/core';
|
|
4
|
+
import { PluginInitOptions } from '../types';
|
|
5
|
+
import { ProductImporter } from '../providers/import-providers/product-importer';
|
|
6
|
+
export declare class ProductImportService implements OnModuleInit {
|
|
7
|
+
private options;
|
|
8
|
+
private jobQueueService;
|
|
9
|
+
private productImporter;
|
|
10
|
+
private searchService;
|
|
11
|
+
private productImportQueue;
|
|
12
|
+
constructor(options: PluginInitOptions, jobQueueService: JobQueueService, productImporter: ProductImporter, searchService: SearchService);
|
|
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<{
|
|
15
|
+
ctx: SerializedRequestContext;
|
|
16
|
+
fileContent: string;
|
|
17
|
+
updateProductSlug: boolean;
|
|
18
|
+
mainLanguage: LanguageCode;
|
|
19
|
+
}>>;
|
|
20
|
+
private triggerProductImport;
|
|
21
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ProductImportService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@vendure/core");
|
|
18
|
+
const constants_1 = require("../constants");
|
|
19
|
+
const product_importer_1 = require("../providers/import-providers/product-importer");
|
|
20
|
+
const lodash_1 = require("lodash");
|
|
21
|
+
let ProductImportService = class ProductImportService {
|
|
22
|
+
options;
|
|
23
|
+
jobQueueService;
|
|
24
|
+
productImporter;
|
|
25
|
+
searchService;
|
|
26
|
+
productImportQueue;
|
|
27
|
+
constructor(options, jobQueueService, productImporter, searchService) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.jobQueueService = jobQueueService;
|
|
30
|
+
this.productImporter = productImporter;
|
|
31
|
+
this.searchService = searchService;
|
|
32
|
+
}
|
|
33
|
+
async onModuleInit() {
|
|
34
|
+
this.productImportQueue = await this.jobQueueService.createQueue({
|
|
35
|
+
name: 'product-import',
|
|
36
|
+
process: async (job) => {
|
|
37
|
+
// Deserialize the RequestContext from the job data
|
|
38
|
+
const ctx = core_1.RequestContext.deserialize(job.data.ctx);
|
|
39
|
+
let jobResult;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
this.productImporter
|
|
42
|
+
.parseAndImport(job.data.fileContent, ctx, job.data.updateProductSlug, job.data.mainLanguage)
|
|
43
|
+
.subscribe({
|
|
44
|
+
next: (result) => {
|
|
45
|
+
const processedCount = result.processed;
|
|
46
|
+
const importedCount = result.imported;
|
|
47
|
+
const percentage = (importedCount / processedCount) * 100;
|
|
48
|
+
job.setProgress(percentage);
|
|
49
|
+
jobResult = result;
|
|
50
|
+
},
|
|
51
|
+
complete: async () => {
|
|
52
|
+
await this.searchService.reindex(ctx);
|
|
53
|
+
resolve((0, lodash_1.omit)(jobResult, 'currentProduct'));
|
|
54
|
+
},
|
|
55
|
+
error: (err) => {
|
|
56
|
+
reject(err);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async processFile(ctx, file, updateProductSlug, mainLanguage) {
|
|
64
|
+
const fileContent = file.buffer.toString('utf-8');
|
|
65
|
+
return this.triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage);
|
|
66
|
+
}
|
|
67
|
+
triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage) {
|
|
68
|
+
return this.productImportQueue.add({
|
|
69
|
+
ctx: ctx.serialize(),
|
|
70
|
+
fileContent,
|
|
71
|
+
updateProductSlug,
|
|
72
|
+
mainLanguage,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
ProductImportService = __decorate([
|
|
77
|
+
(0, common_1.Injectable)(),
|
|
78
|
+
__param(0, (0, common_1.Inject)(constants_1.PRODUCT_IMPORT_EXPORT_PLUGIN_OPTIONS)),
|
|
79
|
+
__metadata("design:paramtypes", [Object, core_1.JobQueueService,
|
|
80
|
+
product_importer_1.ProductImporter,
|
|
81
|
+
core_1.SearchService])
|
|
82
|
+
], ProductImportService);
|
|
83
|
+
exports.ProductImportService = ProductImportService;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ID } from '@vendure/core';
|
|
2
|
+
import { ParsedProduct, ParsedProductWithVariants } from './providers/import-providers/import-parser';
|
|
3
|
+
/**
|
|
4
|
+
* @description
|
|
5
|
+
* The plugin can be configured using the following options:
|
|
6
|
+
*/
|
|
7
|
+
type ImportOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* @description
|
|
10
|
+
* updateProductSlug: If true, the product slug will be updated based on the `name` field in the CSV file.
|
|
11
|
+
*/
|
|
12
|
+
updateProductSlug?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export interface PluginInitOptions {
|
|
15
|
+
importOptions: {
|
|
16
|
+
visibleOptions?: Array<keyof ImportOptions>;
|
|
17
|
+
defaultOptions?: ImportOptions;
|
|
18
|
+
};
|
|
19
|
+
exportOptions: {
|
|
20
|
+
defaultFileName?: string;
|
|
21
|
+
exportAssetsAsOptions?: Array<'url' | 'json'>;
|
|
22
|
+
defaultExportAssetsAs?: 'url' | 'json';
|
|
23
|
+
defaultExportFields?: ExportFields;
|
|
24
|
+
requiredExportFields?: ExportFields;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export type ParsedProductWithId = ParsedProductWithVariants & {
|
|
28
|
+
product: ParsedProduct & {
|
|
29
|
+
productId: number | null;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type JsonAsset = {
|
|
33
|
+
id?: ID;
|
|
34
|
+
url: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
};
|
|
37
|
+
type ExportFields = Array<ProductFields | VariantFields>;
|
|
38
|
+
type ProductFields = 'productId' | 'name' | 'slug' | 'description' | 'assets' | 'facets' | 'optionGroups';
|
|
39
|
+
type VariantFields = 'sku' | 'optionValues' | 'price' | 'taxCategory' | 'stockOnHand' | 'trackInventory' | 'variantAssets' | 'variantFacets';
|
|
40
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ElementRef, AfterViewInit, OnInit } from '@angular/core';
|
|
2
|
+
import { Dialog } from '@vendure/admin-ui/core';
|
|
3
|
+
type ProductFields = 'productId' | 'name' | 'slug' | 'description' | 'assets' | 'facets' | 'optionGroups';
|
|
4
|
+
type VariantFields = 'sku' | 'optionValues' | 'price' | 'taxCategory' | 'stockOnHand' | 'trackInventory' | 'variantAssets' | 'variantFacets';
|
|
5
|
+
type ExportFields = Array<ProductFields | VariantFields>;
|
|
6
|
+
interface PluginInitOptions {
|
|
7
|
+
defaultFileName?: string;
|
|
8
|
+
exportAssetsAsOptions?: Array<'url' | 'json'>;
|
|
9
|
+
defaultExportAssetsAs?: 'url' | 'json';
|
|
10
|
+
defaultExportFields?: ExportFields;
|
|
11
|
+
requiredExportFields?: ExportFields;
|
|
12
|
+
}
|
|
13
|
+
export declare class ExportDialogComponent implements Dialog<{
|
|
14
|
+
result: boolean;
|
|
15
|
+
fileName?: string;
|
|
16
|
+
selectedFields?: string[];
|
|
17
|
+
exportAssetsAs?: 'url' | 'json';
|
|
18
|
+
selectedExportFields?: ExportFields;
|
|
19
|
+
}>, AfterViewInit, OnInit {
|
|
20
|
+
fileNameElement: ElementRef<HTMLInputElement>;
|
|
21
|
+
resolveWith: (result?: {
|
|
22
|
+
result: boolean;
|
|
23
|
+
fileName?: string;
|
|
24
|
+
selectedFields?: string[];
|
|
25
|
+
exportAssetsAs?: 'url' | 'json';
|
|
26
|
+
selectedExportFields?: ExportFields;
|
|
27
|
+
}) => void;
|
|
28
|
+
selection: any[];
|
|
29
|
+
fileName: string;
|
|
30
|
+
customFields: string[];
|
|
31
|
+
selectedFields: string[];
|
|
32
|
+
exportAssetsAs: 'url' | 'json';
|
|
33
|
+
selectedExportFields: ExportFields;
|
|
34
|
+
availableExportFields: ExportFields;
|
|
35
|
+
config: PluginInitOptions;
|
|
36
|
+
ngOnInit(): void;
|
|
37
|
+
ngAfterViewInit(): void;
|
|
38
|
+
export(): void;
|
|
39
|
+
cancel(): void;
|
|
40
|
+
toggleFieldSelection(fieldName: string): void;
|
|
41
|
+
toggleExportFieldSelection(fieldName: ProductFields | VariantFields): void;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<ng-template vdrDialogTitle>{{ 'product-export.dialog.title' | translate }}</ng-template>
|
|
2
|
+
<div class="input-row">
|
|
3
|
+
<vdr-form-field [label]="'product-export.dialog.label' | translate">
|
|
4
|
+
<input
|
|
5
|
+
type="text"
|
|
6
|
+
[(ngModel)]="fileName"
|
|
7
|
+
[placeholder]="config?.defaultFileName || 'products_export.csv'"
|
|
8
|
+
style="width: 100%"
|
|
9
|
+
#fileNameInput
|
|
10
|
+
/>
|
|
11
|
+
</vdr-form-field>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="input-row" style="margin-top: 16px">
|
|
15
|
+
<label style="margin-bottom: 8px; display: inline-block">{{
|
|
16
|
+
'product-export.dialog.customFieldsLabel' | translate
|
|
17
|
+
}}</label>
|
|
18
|
+
<div *ngFor="let field of availableExportFields">
|
|
19
|
+
<input
|
|
20
|
+
type="checkbox"
|
|
21
|
+
[value]="field"
|
|
22
|
+
(change)="toggleExportFieldSelection(field)"
|
|
23
|
+
[checked]="selectedExportFields.includes(field)"
|
|
24
|
+
[disabled]="config.requiredExportFields.includes(field)"
|
|
25
|
+
/>
|
|
26
|
+
{{ field }}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="input-row" style="margin-top: 16px">
|
|
31
|
+
<label style="margin-bottom: 8px; display: inline-block">{{
|
|
32
|
+
'product-export.dialog.customFieldsLabel' | translate
|
|
33
|
+
}}</label>
|
|
34
|
+
<div *ngFor="let field of customFields">
|
|
35
|
+
<input
|
|
36
|
+
type="checkbox"
|
|
37
|
+
[value]="field"
|
|
38
|
+
(change)="toggleFieldSelection(field)"
|
|
39
|
+
[checked]="selectedFields.includes(field)"
|
|
40
|
+
/>
|
|
41
|
+
{{ field }}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<span style="margin-top: 16px; margin-bottom: 8px; display: block">{{
|
|
46
|
+
'product-export.dialog.exportAssetsAs' | translate
|
|
47
|
+
}}</span>
|
|
48
|
+
<div class="input-row" style="margin-top: 8px">
|
|
49
|
+
<label *ngIf="config?.exportAssetsAsOptions?.includes('url')">
|
|
50
|
+
<input
|
|
51
|
+
type="radio"
|
|
52
|
+
name="exportAssets"
|
|
53
|
+
value="url"
|
|
54
|
+
[(ngModel)]="exportAssetsAs"
|
|
55
|
+
style="width: fit-content"
|
|
56
|
+
/>
|
|
57
|
+
{{ 'product-export.dialog.exportAssetsAsOptions.url' | translate }}
|
|
58
|
+
</label>
|
|
59
|
+
<label *ngIf="config?.exportAssetsAsOptions?.includes('json')">
|
|
60
|
+
<input
|
|
61
|
+
type="radio"
|
|
62
|
+
name="exportAssets"
|
|
63
|
+
value="json"
|
|
64
|
+
[(ngModel)]="exportAssetsAs"
|
|
65
|
+
style="width: fit-content"
|
|
66
|
+
/>
|
|
67
|
+
{{ 'product-export.dialog.exportAssetsAsOptions.json' | translate }}
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<ng-template vdrDialogButtons>
|
|
72
|
+
<button type="button" class="btn" (click)="cancel()">
|
|
73
|
+
{{ 'common.cancel' | translate }}
|
|
74
|
+
</button>
|
|
75
|
+
<button type="button" class="btn btn-primary" (click)="export()">
|
|
76
|
+
{{ 'product-export.dialog.export' | translate }}
|
|
77
|
+
</button>
|
|
78
|
+
</ng-template>
|