@haus-tech/product-export-plugin 2.2.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/product-export.controller.d.ts +15 -0
- package/dist/api/product-export.controller.js +121 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/product-export.plugin.d.ts +8 -0
- package/dist/product-export.plugin.js +83 -0
- package/dist/services/product-export.service.d.ts +24 -0
- package/dist/services/product-export.service.js +316 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +2 -0
- package/dist/ui/export-dialog.component.d.ts +33 -0
- package/dist/ui/export-dialog.component.html +62 -0
- package/dist/ui/export-dialog.component.js +75 -0
- package/dist/ui/export-dialog.component.ts +86 -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 +106 -0
- package/dist/ui/providers.d.ts +3 -0
- package/dist/ui/providers.js +40 -0
- package/dist/ui/providers.ts +47 -0
- package/dist/ui/routes.d.ts +2 -0
- package/dist/ui/routes.js +5 -0
- package/dist/ui/routes.ts +3 -0
- package/dist/ui/translations/en.json +17 -0
- package/dist/ui/translations/sv.json +17 -0
- package/package.json +23 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RequestContext, ID } from '@vendure/core';
|
|
2
|
+
import { Response } from 'express';
|
|
3
|
+
import { ProductExportService } from '../services/product-export.service';
|
|
4
|
+
import { PluginInitOptions } from '../types';
|
|
5
|
+
export declare class ProductExportController {
|
|
6
|
+
private options;
|
|
7
|
+
private productExportService;
|
|
8
|
+
constructor(options: PluginInitOptions, productExportService: ProductExportService);
|
|
9
|
+
exportProducts(ctx: RequestContext, res: Response, fileName: string, customFields: string, exportAssetsAs: 'url' | 'json', selection: ID[]): Promise<void>;
|
|
10
|
+
getCustomFields(ctx: RequestContext, ids: string[]): Promise<{
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
}[]>;
|
|
14
|
+
getConfig(): PluginInitOptions;
|
|
15
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.ProductExportController = void 0;
|
|
39
|
+
const core_1 = require("@vendure/core");
|
|
40
|
+
const common_1 = require("@nestjs/common");
|
|
41
|
+
const product_export_service_1 = require("../services/product-export.service");
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const fs_1 = require("fs");
|
|
44
|
+
const constants_1 = require("../constants");
|
|
45
|
+
let ProductExportController = class ProductExportController {
|
|
46
|
+
options;
|
|
47
|
+
productExportService;
|
|
48
|
+
constructor(options, productExportService) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.productExportService = productExportService;
|
|
51
|
+
}
|
|
52
|
+
async exportProducts(ctx, res, fileName, customFields, exportAssetsAs, selection) {
|
|
53
|
+
try {
|
|
54
|
+
if (!selection || !Array.isArray(selection) || selection.length === 0) {
|
|
55
|
+
throw new common_1.UnprocessableEntityException('No products selected');
|
|
56
|
+
}
|
|
57
|
+
if (!fileName) {
|
|
58
|
+
if (this.options.defaultFileName && !this.options.defaultFileName.endsWith('.csv')) {
|
|
59
|
+
fileName = this.options.defaultFileName += '.csv';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
fileName = this.options.defaultFileName || 'products_export.csv';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (!fileName.endsWith('.csv')) {
|
|
66
|
+
fileName += '.csv';
|
|
67
|
+
}
|
|
68
|
+
const csv = await this.productExportService.createExportFile(ctx, selection, fileName, customFields, exportAssetsAs);
|
|
69
|
+
const readStream = fs.createReadStream(csv);
|
|
70
|
+
res.set({
|
|
71
|
+
'Content-Type': 'text/csv',
|
|
72
|
+
'Content-Disposition': `attachment; filename=${fileName}`,
|
|
73
|
+
});
|
|
74
|
+
readStream.on('end', async () => {
|
|
75
|
+
await fs_1.promises.unlink(csv);
|
|
76
|
+
});
|
|
77
|
+
readStream.pipe(res);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
throw new common_1.UnprocessableEntityException(e.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async getCustomFields(ctx, ids) {
|
|
84
|
+
return this.productExportService.getCustomFields(ctx, ids);
|
|
85
|
+
}
|
|
86
|
+
getConfig() {
|
|
87
|
+
return this.options;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
__decorate([
|
|
91
|
+
(0, common_1.Post)('export'),
|
|
92
|
+
__param(0, (0, core_1.Ctx)()),
|
|
93
|
+
__param(1, (0, common_1.Res)()),
|
|
94
|
+
__param(2, (0, common_1.Query)('fileName')),
|
|
95
|
+
__param(3, (0, common_1.Query)('customFields')),
|
|
96
|
+
__param(4, (0, common_1.Query)('exportAssetsAs')),
|
|
97
|
+
__param(5, (0, common_1.Body)()),
|
|
98
|
+
__metadata("design:type", Function),
|
|
99
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Object, String, String, String, Array]),
|
|
100
|
+
__metadata("design:returntype", Promise)
|
|
101
|
+
], ProductExportController.prototype, "exportProducts", null);
|
|
102
|
+
__decorate([
|
|
103
|
+
(0, common_1.Post)('custom-fields'),
|
|
104
|
+
__param(0, (0, core_1.Ctx)()),
|
|
105
|
+
__param(1, (0, common_1.Body)()),
|
|
106
|
+
__metadata("design:type", Function),
|
|
107
|
+
__metadata("design:paramtypes", [core_1.RequestContext, Array]),
|
|
108
|
+
__metadata("design:returntype", Promise)
|
|
109
|
+
], ProductExportController.prototype, "getCustomFields", null);
|
|
110
|
+
__decorate([
|
|
111
|
+
(0, common_1.Get)('config'),
|
|
112
|
+
__metadata("design:type", Function),
|
|
113
|
+
__metadata("design:paramtypes", []),
|
|
114
|
+
__metadata("design:returntype", void 0)
|
|
115
|
+
], ProductExportController.prototype, "getConfig", null);
|
|
116
|
+
ProductExportController = __decorate([
|
|
117
|
+
(0, common_1.Controller)('product-export'),
|
|
118
|
+
__param(0, (0, common_1.Inject)(constants_1.PRODUCT_EXPORT_PLUGIN_OPTIONS)),
|
|
119
|
+
__metadata("design:paramtypes", [Object, product_export_service_1.ProductExportService])
|
|
120
|
+
], ProductExportController);
|
|
121
|
+
exports.ProductExportController = ProductExportController;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loggerCtx = exports.PRODUCT_EXPORT_PLUGIN_OPTIONS = void 0;
|
|
4
|
+
exports.PRODUCT_EXPORT_PLUGIN_OPTIONS = Symbol('PRODUCT_EXPORT_PLUGIN_OPTIONS');
|
|
5
|
+
exports.loggerCtx = 'ProductExportPlugin';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
|
|
2
|
+
import { Type } from '@vendure/core';
|
|
3
|
+
import { PluginInitOptions } from './types';
|
|
4
|
+
export declare class ProductExportPlugin {
|
|
5
|
+
static options: PluginInitOptions;
|
|
6
|
+
static init(options: PluginInitOptions): Type<ProductExportPlugin>;
|
|
7
|
+
static ui: AdminUiExtension;
|
|
8
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 ProductExportPlugin_1;
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.ProductExportPlugin = void 0;
|
|
34
|
+
const path = __importStar(require("path"));
|
|
35
|
+
const core_1 = require("@vendure/core");
|
|
36
|
+
const constants_1 = require("./constants");
|
|
37
|
+
const product_export_service_1 = require("./services/product-export.service");
|
|
38
|
+
const product_export_controller_1 = require("./api/product-export.controller");
|
|
39
|
+
let ProductExportPlugin = ProductExportPlugin_1 = class ProductExportPlugin {
|
|
40
|
+
static options;
|
|
41
|
+
static init(options) {
|
|
42
|
+
if (!options.defaultFileName) {
|
|
43
|
+
options.defaultFileName = 'products_export.csv';
|
|
44
|
+
}
|
|
45
|
+
if (!options.exportAssetsAsOptions) {
|
|
46
|
+
options.exportAssetsAsOptions = ['url', 'json'];
|
|
47
|
+
}
|
|
48
|
+
if (!options.defaultExportAssetsAs) {
|
|
49
|
+
options.defaultExportAssetsAs = 'url';
|
|
50
|
+
}
|
|
51
|
+
this.options = options;
|
|
52
|
+
return ProductExportPlugin_1;
|
|
53
|
+
}
|
|
54
|
+
static ui = {
|
|
55
|
+
id: 'product-export-ui',
|
|
56
|
+
extensionPath: path.join(__dirname, 'ui'),
|
|
57
|
+
translations: {
|
|
58
|
+
en: path.join(__dirname, 'ui/translations/en.json'),
|
|
59
|
+
sv: path.join(__dirname, 'ui/translations/sv.json'),
|
|
60
|
+
},
|
|
61
|
+
routes: [{ route: 'product-export', filePath: 'routes.ts' }],
|
|
62
|
+
providers: ['providers.ts'],
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
ProductExportPlugin = ProductExportPlugin_1 = __decorate([
|
|
66
|
+
(0, core_1.VendurePlugin)({
|
|
67
|
+
imports: [core_1.PluginCommonModule],
|
|
68
|
+
providers: [
|
|
69
|
+
{ provide: constants_1.PRODUCT_EXPORT_PLUGIN_OPTIONS, useFactory: () => ProductExportPlugin_1.options },
|
|
70
|
+
product_export_service_1.ProductExportService,
|
|
71
|
+
],
|
|
72
|
+
controllers: [product_export_controller_1.ProductExportController],
|
|
73
|
+
configuration: (config) => {
|
|
74
|
+
// Plugin-specific configuration
|
|
75
|
+
// such as custom fields, custom permissions,
|
|
76
|
+
// strategies etc. can be configured here by
|
|
77
|
+
// modifying the `config` object.
|
|
78
|
+
return config;
|
|
79
|
+
},
|
|
80
|
+
compatibility: '^2.0.0',
|
|
81
|
+
})
|
|
82
|
+
], ProductExportPlugin);
|
|
83
|
+
exports.ProductExportPlugin = ProductExportPlugin;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EntityHydrator, ID, ProductService, RequestContext, StockLevelService, ChannelService, ProductVariantService, ConfigService } from '@vendure/core';
|
|
2
|
+
import { PluginInitOptions } from '../types';
|
|
3
|
+
export type RelationType = 'string' | 'boolean' | 'localeString' | 'text' | 'localeText' | 'int' | 'float' | 'datetime' | 'relation';
|
|
4
|
+
export declare class ProductExportService {
|
|
5
|
+
private options;
|
|
6
|
+
private productService;
|
|
7
|
+
private variantService;
|
|
8
|
+
private stockLevelService;
|
|
9
|
+
private entityHydratorService;
|
|
10
|
+
private channelService;
|
|
11
|
+
private configService;
|
|
12
|
+
constructor(options: PluginInitOptions, productService: ProductService, variantService: ProductVariantService, stockLevelService: StockLevelService, entityHydratorService: EntityHydrator, channelService: ChannelService, configService: ConfigService);
|
|
13
|
+
createExportFile(ctx: RequestContext, selectionIds: ID[], fileName: string, selectedCustomFields: string, exportAssetsAs: 'url' | 'json'): Promise<string>;
|
|
14
|
+
private handleAssets;
|
|
15
|
+
private handleCustomFields;
|
|
16
|
+
private getStockOnHand;
|
|
17
|
+
private mapTranslations;
|
|
18
|
+
private mapFacetTranslations;
|
|
19
|
+
getCustomFields(ctx: RequestContext, productIds: string[]): Promise<{
|
|
20
|
+
name: string;
|
|
21
|
+
type: string;
|
|
22
|
+
}[]>;
|
|
23
|
+
getConfig(): Promise<PluginInitOptions>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
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
|
+
variantService;
|
|
53
|
+
stockLevelService;
|
|
54
|
+
entityHydratorService;
|
|
55
|
+
channelService;
|
|
56
|
+
configService;
|
|
57
|
+
constructor(options, productService, variantService, stockLevelService, entityHydratorService, channelService, configService) {
|
|
58
|
+
this.options = options;
|
|
59
|
+
this.productService = productService;
|
|
60
|
+
this.variantService = variantService;
|
|
61
|
+
this.stockLevelService = stockLevelService;
|
|
62
|
+
this.entityHydratorService = entityHydratorService;
|
|
63
|
+
this.channelService = channelService;
|
|
64
|
+
this.configService = configService;
|
|
65
|
+
}
|
|
66
|
+
async createExportFile(ctx, selectionIds, fileName, selectedCustomFields, exportAssetsAs) {
|
|
67
|
+
const products = await this.productService.findByIds(ctx, selectionIds);
|
|
68
|
+
const channel = await this.channelService.findOne(ctx, ctx.channelId);
|
|
69
|
+
if (!channel) {
|
|
70
|
+
throw new Error('Channel not found');
|
|
71
|
+
}
|
|
72
|
+
const languages = channel.availableLanguageCodes;
|
|
73
|
+
const allCustomFieldNames = await this.getCustomFields(ctx, selectionIds);
|
|
74
|
+
const filteredCustomFieldNames = selectedCustomFields
|
|
75
|
+
.split(',')
|
|
76
|
+
.filter((field) => allCustomFieldNames.some((f) => f.name === field))
|
|
77
|
+
.map((field) => {
|
|
78
|
+
const found = allCustomFieldNames.find((f) => f.name === field);
|
|
79
|
+
return found ? `${found.name}:${found.type}` : '';
|
|
80
|
+
});
|
|
81
|
+
const exportFile = path.join(os_1.default.tmpdir(), fileName);
|
|
82
|
+
const headers = [];
|
|
83
|
+
headers.push({ id: 'productId', title: 'productId' });
|
|
84
|
+
// Add headers for translations
|
|
85
|
+
for (const lang of languages) {
|
|
86
|
+
headers.push({ id: `name:${lang}`, title: `name:${lang}` });
|
|
87
|
+
headers.push({ id: `slug:${lang}`, title: `slug:${lang}` });
|
|
88
|
+
headers.push({ id: `description:${lang}`, title: `description:${lang}` });
|
|
89
|
+
}
|
|
90
|
+
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) => ({
|
|
91
|
+
id: field,
|
|
92
|
+
title: field,
|
|
93
|
+
})));
|
|
94
|
+
const csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
|
|
95
|
+
path: exportFile,
|
|
96
|
+
header: headers,
|
|
97
|
+
});
|
|
98
|
+
for (const product of products) {
|
|
99
|
+
const records = [];
|
|
100
|
+
const hydratedProduct = await this.entityHydratorService.hydrate(ctx, product, {
|
|
101
|
+
relations: [
|
|
102
|
+
'variants',
|
|
103
|
+
'facetValues',
|
|
104
|
+
'facetValues.facet',
|
|
105
|
+
'optionGroups',
|
|
106
|
+
'assets',
|
|
107
|
+
'variants.assets',
|
|
108
|
+
'variants.facetValues',
|
|
109
|
+
'variants.facetValues.facet',
|
|
110
|
+
'variants.options',
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const { assets = [], facetValues = [], optionGroups = [], variants = [], translations = [], customFields = {}, } = hydratedProduct;
|
|
114
|
+
const nameTranslations = this.mapTranslations(translations, 'name', languages);
|
|
115
|
+
const slugTranslations = this.mapTranslations(translations, 'slug', languages);
|
|
116
|
+
const descriptionTranslations = this.mapTranslations(translations, 'description', languages);
|
|
117
|
+
// Filter out all variants that are soft deleted
|
|
118
|
+
const activeVariants = variants.filter((v) => !v.deletedAt);
|
|
119
|
+
const productAssets = assets.length === 0 ? '' :
|
|
120
|
+
assets.length > 0
|
|
121
|
+
? this.handleAssets(assets.map(({ asset }) => asset), exportAssetsAs)
|
|
122
|
+
: this.handleAssets([hydratedProduct.featuredAsset], exportAssetsAs);
|
|
123
|
+
const productFacets = languages.reduce((acc, lang) => {
|
|
124
|
+
acc[lang] = facetValues
|
|
125
|
+
.map((facetValue) => this.mapFacetTranslations(facetValue, lang))
|
|
126
|
+
.join('|');
|
|
127
|
+
return acc;
|
|
128
|
+
}, {});
|
|
129
|
+
const optionGroupNames = languages.reduce((acc, lang) => {
|
|
130
|
+
acc[lang] = (0, lodash_1.sortBy)(optionGroups, (g) => g.id)
|
|
131
|
+
.map((group) => this.mapTranslations(group.translations, 'name', [lang])[lang])
|
|
132
|
+
.join('|');
|
|
133
|
+
return acc;
|
|
134
|
+
}, {});
|
|
135
|
+
for (const variant of activeVariants) {
|
|
136
|
+
const variantValues = languages.reduce((acc, lang) => {
|
|
137
|
+
acc[lang] = ((0, lodash_1.sortBy)(variant.options, (o) => o.groupId) || [])
|
|
138
|
+
.map((option) => this.mapTranslations(option.translations, 'name', [lang])[lang])
|
|
139
|
+
.join('|');
|
|
140
|
+
return acc;
|
|
141
|
+
}, {});
|
|
142
|
+
const variantAssets = this.handleAssets(variant.assets.map(({ asset }) => asset), exportAssetsAs);
|
|
143
|
+
const variantFacets = languages.reduce((acc, lang) => {
|
|
144
|
+
acc[lang] = (variant.facetValues || [])
|
|
145
|
+
.map((facet) => this.mapFacetTranslations(facet, lang))
|
|
146
|
+
.join('|');
|
|
147
|
+
return acc;
|
|
148
|
+
}, {});
|
|
149
|
+
const stockOnHand = await this.getStockOnHand(ctx, variant.id); // Adjusted to fetch stock details
|
|
150
|
+
const variantTranslations = variant.translations;
|
|
151
|
+
const variantNameTranslations = this.mapTranslations(variantTranslations, 'name', languages);
|
|
152
|
+
const record = {};
|
|
153
|
+
for (const lang of languages) {
|
|
154
|
+
const escapedDescription = descriptionTranslations[lang].replace(/"/g, "'").replace(/\s+/g, ' ').replace(/,\s*'/g, ", '").replace(/'/g, "''");
|
|
155
|
+
const escapedName = nameTranslations[lang].replace(/"/g, "'").replace(/\s+/g, ' ').replace(/,\s*'/g, ", '").replace(/'/g, "''");
|
|
156
|
+
record.productId = records.length === 0 ? product.id : '';
|
|
157
|
+
record[`name:${lang}`] = records.length === 0 ? escapedName || '' : '';
|
|
158
|
+
record[`slug:${lang}`] = records.length === 0 ? slugTranslations[lang] || '' : '';
|
|
159
|
+
record[`description:${lang}`] =
|
|
160
|
+
records.length === 0 ? escapedDescription || '' : '';
|
|
161
|
+
record[`facets:${lang}`] = records.length === 0 ? productFacets[lang] : '';
|
|
162
|
+
record[`optionGroups:${lang}`] = records.length === 0 ? optionGroupNames[lang] : '';
|
|
163
|
+
record[`optionValues:${lang}`] = variantValues[lang];
|
|
164
|
+
record[`variantFacets:${lang}`] = variantFacets[lang];
|
|
165
|
+
}
|
|
166
|
+
record.assets = records.length === 0 ? productAssets : '';
|
|
167
|
+
record.sku = variant.sku;
|
|
168
|
+
record.price = variant.productVariantPrices[0]?.price / 100; // Assuming the price is stored in cents
|
|
169
|
+
record.taxCategory = 'standard'; // Replace with actual tax category if available
|
|
170
|
+
record.stockOnHand = stockOnHand;
|
|
171
|
+
record.trackInventory = variant.trackInventory.toLowerCase();
|
|
172
|
+
record.variantAssets = variantAssets;
|
|
173
|
+
for (const field of filteredCustomFieldNames) {
|
|
174
|
+
const [entity, fieldName, type] = field.split(':');
|
|
175
|
+
if (entity === 'product') {
|
|
176
|
+
record[field] =
|
|
177
|
+
this.handleCustomFields(customFields, fieldName, type, exportAssetsAs) || '';
|
|
178
|
+
}
|
|
179
|
+
else if (entity === 'variant') {
|
|
180
|
+
record[field] =
|
|
181
|
+
this.handleCustomFields(variant.customFields, fieldName, type, exportAssetsAs) || '';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
records.push(record);
|
|
185
|
+
}
|
|
186
|
+
await csvWriter.writeRecords(records);
|
|
187
|
+
}
|
|
188
|
+
return exportFile;
|
|
189
|
+
}
|
|
190
|
+
handleAssets(assets, exportAssetsAs) {
|
|
191
|
+
if (!assets.length) {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
if (exportAssetsAs === 'url') {
|
|
195
|
+
return assets.map((asset) => asset.source).join('|');
|
|
196
|
+
}
|
|
197
|
+
return JSON.stringify(assets.length > 1
|
|
198
|
+
? assets.map((asset) => ({
|
|
199
|
+
id: asset.id,
|
|
200
|
+
name: asset.name,
|
|
201
|
+
url: asset.source,
|
|
202
|
+
}))
|
|
203
|
+
: {
|
|
204
|
+
id: assets[0].id,
|
|
205
|
+
name: assets[0].name,
|
|
206
|
+
url: assets[0].source,
|
|
207
|
+
})
|
|
208
|
+
.replace(/"/g, "'") // Replace double quotes with single quotes
|
|
209
|
+
.replace(/\s+/g, ' ') // Remove unnecessary whitespace and line breaks
|
|
210
|
+
.replace(/,\s*'/g, ", '"); // Ensure clean spacing after commas
|
|
211
|
+
}
|
|
212
|
+
handleCustomFields(customFields, fieldName, type, exportAssetsAs) {
|
|
213
|
+
const fieldValue = customFields[fieldName];
|
|
214
|
+
if (!fieldValue) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const relationsTypes = [
|
|
218
|
+
...this.configService.customFields.Product.map((field) => {
|
|
219
|
+
if (field.type === 'relation')
|
|
220
|
+
return field.entity.name.toLowerCase();
|
|
221
|
+
}),
|
|
222
|
+
...this.configService.customFields.ProductVariant.map((field) => {
|
|
223
|
+
if (field.type === 'relation')
|
|
224
|
+
return field.entity.name.toLowerCase();
|
|
225
|
+
}),
|
|
226
|
+
].filter((type) => type);
|
|
227
|
+
if (relationsTypes.includes(type)) {
|
|
228
|
+
if (type === 'asset') {
|
|
229
|
+
return (customFields[fieldName] = this.handleAssets([fieldValue], exportAssetsAs));
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
return (customFields[fieldName] = fieldValue.id);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (typeof fieldValue === 'object') {
|
|
237
|
+
customFields[fieldName] = customFields[fieldName] = JSON.stringify(fieldValue)
|
|
238
|
+
.replace(/"/g, "'") // Replace double quotes with single quotes
|
|
239
|
+
.replace(/\s+/g, ' ') // Remove unnecessary whitespace and line breaks
|
|
240
|
+
.replace(/,\s*'/g, ", '"); // Ensure clean spacing after commas
|
|
241
|
+
}
|
|
242
|
+
else if ((0, lodash_1.startsWith)(fieldValue, '{') || (0, lodash_1.startsWith)(fieldValue, '[')) {
|
|
243
|
+
customFields[fieldName] = fieldValue.replace(/"/g, "'").replace(/\s+/g, ' ');
|
|
244
|
+
return customFields[fieldName];
|
|
245
|
+
}
|
|
246
|
+
return customFields[fieldName];
|
|
247
|
+
}
|
|
248
|
+
// Method to fetch stock on hand for a variant
|
|
249
|
+
async getStockOnHand(ctx, variantId) {
|
|
250
|
+
const stockLevel = await this.stockLevelService.getAvailableStock(ctx, variantId);
|
|
251
|
+
return stockLevel.stockOnHand;
|
|
252
|
+
}
|
|
253
|
+
// Method to map translations for a specific field
|
|
254
|
+
mapTranslations(translations, field, languages) {
|
|
255
|
+
const translationMap = {};
|
|
256
|
+
for (const lang of languages) {
|
|
257
|
+
const translation = translations.find((t) => t.languageCode === lang);
|
|
258
|
+
translationMap[lang] = translation ? translation[field] : '';
|
|
259
|
+
}
|
|
260
|
+
return translationMap;
|
|
261
|
+
}
|
|
262
|
+
// Method to map facet translations
|
|
263
|
+
mapFacetTranslations(facetValue, lang) {
|
|
264
|
+
const facetNameTranslations = this.mapTranslations(facetValue.facet.translations, 'name', [
|
|
265
|
+
lang,
|
|
266
|
+
]);
|
|
267
|
+
const facetValueTranslations = this.mapTranslations(facetValue.translations, 'name', [lang]);
|
|
268
|
+
return `${facetNameTranslations[lang]}:${facetValueTranslations[lang]}`;
|
|
269
|
+
}
|
|
270
|
+
async getCustomFields(ctx, productIds) {
|
|
271
|
+
const customFields = new Set();
|
|
272
|
+
(0, lodash_1.forEach)(this.configService.customFields.Product, (field) => {
|
|
273
|
+
if (field.type === 'relation') {
|
|
274
|
+
const entity = field.entity.name;
|
|
275
|
+
customFields.add({
|
|
276
|
+
name: `variant:${field.name}`,
|
|
277
|
+
type: entity.toLowerCase(),
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
customFields.add({
|
|
282
|
+
name: `variant:${field.name}`,
|
|
283
|
+
type: field.type,
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
(0, lodash_1.forEach)(this.configService.customFields.ProductVariant, (field) => {
|
|
287
|
+
if (field.type === 'relation') {
|
|
288
|
+
const entity = field.entity.name;
|
|
289
|
+
customFields.add({
|
|
290
|
+
name: `variant:${field.name}`,
|
|
291
|
+
type: entity.toLowerCase(),
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
customFields.add({
|
|
296
|
+
name: `variant:${field.name}`,
|
|
297
|
+
type: field.type,
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
return Array.from(customFields);
|
|
301
|
+
}
|
|
302
|
+
async getConfig() {
|
|
303
|
+
return this.options;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
ProductExportService = __decorate([
|
|
307
|
+
(0, common_1.Injectable)(),
|
|
308
|
+
__param(0, (0, common_1.Inject)(constants_1.PRODUCT_EXPORT_PLUGIN_OPTIONS)),
|
|
309
|
+
__metadata("design:paramtypes", [Object, core_1.ProductService,
|
|
310
|
+
core_1.ProductVariantService,
|
|
311
|
+
core_1.StockLevelService,
|
|
312
|
+
core_1.EntityHydrator,
|
|
313
|
+
core_1.ChannelService,
|
|
314
|
+
core_1.ConfigService])
|
|
315
|
+
], ProductExportService);
|
|
316
|
+
exports.ProductExportService = ProductExportService;
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ElementRef, AfterViewInit, OnInit } from '@angular/core';
|
|
2
|
+
import { Dialog } from '@vendure/admin-ui/core';
|
|
3
|
+
interface PluginInitOptions {
|
|
4
|
+
defaultFileName?: string;
|
|
5
|
+
exportAssetsAsOptions?: Array<'url' | 'json'>;
|
|
6
|
+
defaultExportAssetsAs?: 'url' | 'json';
|
|
7
|
+
}
|
|
8
|
+
export declare class ExportDialogComponent implements Dialog<{
|
|
9
|
+
result: boolean;
|
|
10
|
+
fileName?: string;
|
|
11
|
+
selectedFields?: string[];
|
|
12
|
+
exportAssetsAs?: 'url' | 'json';
|
|
13
|
+
}>, AfterViewInit, OnInit {
|
|
14
|
+
fileNameElement: ElementRef<HTMLInputElement>;
|
|
15
|
+
resolveWith: (result?: {
|
|
16
|
+
result: boolean;
|
|
17
|
+
fileName?: string;
|
|
18
|
+
selectedFields?: string[];
|
|
19
|
+
exportAssetsAs?: 'url' | 'json';
|
|
20
|
+
}) => void;
|
|
21
|
+
selection: any[];
|
|
22
|
+
fileName: string;
|
|
23
|
+
customFields: string[];
|
|
24
|
+
selectedFields: string[];
|
|
25
|
+
exportAssetsAs: 'url' | 'json';
|
|
26
|
+
config: PluginInitOptions;
|
|
27
|
+
ngOnInit(): void;
|
|
28
|
+
ngAfterViewInit(): void;
|
|
29
|
+
export(): void;
|
|
30
|
+
cancel(): void;
|
|
31
|
+
toggleFieldSelection(fieldName: string): void;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
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 customFields">
|
|
19
|
+
<input
|
|
20
|
+
type="checkbox"
|
|
21
|
+
[value]="field"
|
|
22
|
+
(change)="toggleFieldSelection(field)"
|
|
23
|
+
[checked]="selectedFields.includes(field)"
|
|
24
|
+
/>
|
|
25
|
+
{{ field }}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<span style="margin-top: 16px; margin-bottom: 8px; display: block">{{
|
|
30
|
+
'product-export.dialog.exportAssetsAs' | translate
|
|
31
|
+
}}</span>
|
|
32
|
+
<div class="input-row" style="margin-top: 8px">
|
|
33
|
+
<label *ngIf="config?.exportAssetsAsOptions?.includes('url')">
|
|
34
|
+
<input
|
|
35
|
+
type="radio"
|
|
36
|
+
name="exportAssets"
|
|
37
|
+
value="url"
|
|
38
|
+
[(ngModel)]="exportAssetsAs"
|
|
39
|
+
style="width: fit-content"
|
|
40
|
+
/>
|
|
41
|
+
{{ 'product-export.dialog.exportAssetsAsOptions.url' | translate }}
|
|
42
|
+
</label>
|
|
43
|
+
<label *ngIf="config?.exportAssetsAsOptions?.includes('json')">
|
|
44
|
+
<input
|
|
45
|
+
type="radio"
|
|
46
|
+
name="exportAssets"
|
|
47
|
+
value="json"
|
|
48
|
+
[(ngModel)]="exportAssetsAs"
|
|
49
|
+
style="width: fit-content"
|
|
50
|
+
/>
|
|
51
|
+
{{ 'product-export.dialog.exportAssetsAsOptions.json' | translate }}
|
|
52
|
+
</label>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<ng-template vdrDialogButtons>
|
|
56
|
+
<button type="button" class="btn" (click)="cancel()">
|
|
57
|
+
{{ 'common.cancel' | translate }}
|
|
58
|
+
</button>
|
|
59
|
+
<button type="button" class="btn btn-primary" (click)="export()">
|
|
60
|
+
{{ 'product-export.dialog.export' | translate }}
|
|
61
|
+
</button>
|
|
62
|
+
</ng-template>
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ExportDialogComponent = void 0;
|
|
13
|
+
const core_1 = require("@angular/core");
|
|
14
|
+
const forms_1 = require("@angular/forms");
|
|
15
|
+
const core_2 = require("@vendure/admin-ui/core");
|
|
16
|
+
let ExportDialogComponent = class ExportDialogComponent {
|
|
17
|
+
fileNameElement;
|
|
18
|
+
resolveWith;
|
|
19
|
+
selection = [];
|
|
20
|
+
fileName = '';
|
|
21
|
+
customFields = [];
|
|
22
|
+
selectedFields = [];
|
|
23
|
+
exportAssetsAs = 'url';
|
|
24
|
+
config;
|
|
25
|
+
ngOnInit() {
|
|
26
|
+
this.selectedFields = [...this.customFields];
|
|
27
|
+
if (this.config.defaultExportAssetsAs?.includes(this.config.defaultExportAssetsAs)) {
|
|
28
|
+
this.exportAssetsAs = this.config.defaultExportAssetsAs;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.exportAssetsAs = this.config.exportAssetsAsOptions?.[0] || 'url';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ngAfterViewInit() {
|
|
35
|
+
if (this.fileNameElement) {
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
this.fileNameElement.nativeElement.focus();
|
|
38
|
+
}, 0);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export() {
|
|
42
|
+
this.fileName = this.fileName?.trim();
|
|
43
|
+
this.resolveWith({
|
|
44
|
+
result: true,
|
|
45
|
+
fileName: this.fileName,
|
|
46
|
+
selectedFields: this.selectedFields,
|
|
47
|
+
exportAssetsAs: this.exportAssetsAs,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
cancel() {
|
|
51
|
+
this.resolveWith({ result: false });
|
|
52
|
+
}
|
|
53
|
+
toggleFieldSelection(fieldName) {
|
|
54
|
+
const index = this.selectedFields.indexOf(fieldName);
|
|
55
|
+
if (index > -1) {
|
|
56
|
+
this.selectedFields.splice(index, 1);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.selectedFields.push(fieldName);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
__decorate([
|
|
64
|
+
(0, core_1.ViewChild)('fileNameInput', { static: false }),
|
|
65
|
+
__metadata("design:type", core_1.ElementRef)
|
|
66
|
+
], ExportDialogComponent.prototype, "fileNameElement", void 0);
|
|
67
|
+
ExportDialogComponent = __decorate([
|
|
68
|
+
(0, core_1.Component)({
|
|
69
|
+
selector: 'vdr-export-dialog',
|
|
70
|
+
templateUrl: './export-dialog.component.html',
|
|
71
|
+
standalone: true,
|
|
72
|
+
imports: [core_2.SharedModule, forms_1.FormsModule],
|
|
73
|
+
})
|
|
74
|
+
], ExportDialogComponent);
|
|
75
|
+
exports.ExportDialogComponent = ExportDialogComponent;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'
|
|
2
|
+
import { Dialog } from '@vendure/admin-ui/core'
|
|
3
|
+
import { FormsModule } from '@angular/forms'
|
|
4
|
+
import { SharedModule } from '@vendure/admin-ui/core'
|
|
5
|
+
import { Apollo } from 'apollo-angular'
|
|
6
|
+
import gql from 'graphql-tag'
|
|
7
|
+
|
|
8
|
+
interface PluginInitOptions {
|
|
9
|
+
defaultFileName?: string
|
|
10
|
+
exportAssetsAsOptions?: Array<'url' | 'json'>
|
|
11
|
+
defaultExportAssetsAs?: 'url' | 'json'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Component({
|
|
15
|
+
selector: 'vdr-export-dialog',
|
|
16
|
+
templateUrl: './export-dialog.component.html',
|
|
17
|
+
standalone: true,
|
|
18
|
+
imports: [SharedModule, FormsModule],
|
|
19
|
+
})
|
|
20
|
+
export class ExportDialogComponent
|
|
21
|
+
implements
|
|
22
|
+
Dialog<{
|
|
23
|
+
result: boolean
|
|
24
|
+
fileName?: string
|
|
25
|
+
selectedFields?: string[]
|
|
26
|
+
exportAssetsAs?: 'url' | 'json'
|
|
27
|
+
}>,
|
|
28
|
+
AfterViewInit,
|
|
29
|
+
OnInit
|
|
30
|
+
{
|
|
31
|
+
@ViewChild('fileNameInput', { static: false }) fileNameElement: ElementRef<HTMLInputElement>
|
|
32
|
+
|
|
33
|
+
resolveWith: (result?: {
|
|
34
|
+
result: boolean
|
|
35
|
+
fileName?: string
|
|
36
|
+
selectedFields?: string[]
|
|
37
|
+
exportAssetsAs?: 'url' | 'json'
|
|
38
|
+
}) => void
|
|
39
|
+
selection: any[] = []
|
|
40
|
+
fileName: string = ''
|
|
41
|
+
customFields: string[] = []
|
|
42
|
+
selectedFields: string[] = []
|
|
43
|
+
exportAssetsAs: 'url' | 'json' = 'url'
|
|
44
|
+
|
|
45
|
+
config: PluginInitOptions
|
|
46
|
+
|
|
47
|
+
ngOnInit(): void {
|
|
48
|
+
this.selectedFields = [...this.customFields]
|
|
49
|
+
if (this.config.defaultExportAssetsAs?.includes(this.config.defaultExportAssetsAs)) {
|
|
50
|
+
this.exportAssetsAs = this.config.defaultExportAssetsAs
|
|
51
|
+
} else {
|
|
52
|
+
this.exportAssetsAs = this.config.exportAssetsAsOptions?.[0] || 'url'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ngAfterViewInit(): void {
|
|
57
|
+
if (this.fileNameElement) {
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
this.fileNameElement.nativeElement.focus()
|
|
60
|
+
}, 0)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export() {
|
|
65
|
+
this.fileName = this.fileName?.trim()
|
|
66
|
+
this.resolveWith({
|
|
67
|
+
result: true,
|
|
68
|
+
fileName: this.fileName,
|
|
69
|
+
selectedFields: this.selectedFields,
|
|
70
|
+
exportAssetsAs: this.exportAssetsAs,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
cancel() {
|
|
75
|
+
this.resolveWith({ result: false })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
toggleFieldSelection(fieldName: string) {
|
|
79
|
+
const index = this.selectedFields.indexOf(fieldName)
|
|
80
|
+
if (index > -1) {
|
|
81
|
+
this.selectedFields.splice(index, 1)
|
|
82
|
+
} else {
|
|
83
|
+
this.selectedFields.push(fieldName)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DataService, LocalStorageService, NotificationService } from '@vendure/admin-ui/core';
|
|
2
|
+
import { Product } from '@vendure/core';
|
|
3
|
+
export declare class ProductExportService {
|
|
4
|
+
protected dataService: DataService;
|
|
5
|
+
private notificationService;
|
|
6
|
+
private localStorageService;
|
|
7
|
+
serverPath: string;
|
|
8
|
+
constructor(dataService: DataService, notificationService: NotificationService, localStorageService: LocalStorageService);
|
|
9
|
+
getCustomFields(productIds: string[]): Promise<string[]>;
|
|
10
|
+
getConfig(): Promise<any>;
|
|
11
|
+
exportProducts(selection: Product[], fileName?: string, customFields?: string[], exportAssetsAs?: 'url' | 'json'): Promise<void>;
|
|
12
|
+
private getHeaders;
|
|
13
|
+
private downloadBlob;
|
|
14
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ProductExportService = void 0;
|
|
13
|
+
const core_1 = require("@angular/core");
|
|
14
|
+
const core_2 = require("@vendure/admin-ui/core");
|
|
15
|
+
let ProductExportService = class ProductExportService {
|
|
16
|
+
dataService;
|
|
17
|
+
notificationService;
|
|
18
|
+
localStorageService;
|
|
19
|
+
serverPath;
|
|
20
|
+
constructor(dataService, notificationService, localStorageService) {
|
|
21
|
+
this.dataService = dataService;
|
|
22
|
+
this.notificationService = notificationService;
|
|
23
|
+
this.localStorageService = localStorageService;
|
|
24
|
+
this.serverPath = (0, core_2.getServerLocation)();
|
|
25
|
+
}
|
|
26
|
+
async getCustomFields(productIds) {
|
|
27
|
+
return fetch(`${this.serverPath}/product-export/custom-fields`, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
...this.getHeaders(),
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(productIds),
|
|
34
|
+
})
|
|
35
|
+
.then((res) => res.json())
|
|
36
|
+
.then((data) => {
|
|
37
|
+
return data.map((field) => field.name);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async getConfig() {
|
|
41
|
+
return fetch(`${this.serverPath}/product-export/config`, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
...this.getHeaders(),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
.then((res) => res.json())
|
|
49
|
+
.then((data) => data);
|
|
50
|
+
}
|
|
51
|
+
async exportProducts(selection, fileName, customFields, exportAssetsAs) {
|
|
52
|
+
this.notificationService.info(`Exporting ${selection.length} products to ${fileName}.csv`);
|
|
53
|
+
const productIds = selection.map((product) => product.id);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${this.serverPath}/product-export/export?fileName=${fileName}&customFields=${customFields}&exportAssetsAs=${exportAssetsAs}`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
...this.getHeaders(),
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(productIds),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
const json = await res.json();
|
|
65
|
+
throw Error(json?.message);
|
|
66
|
+
}
|
|
67
|
+
const header = res.headers.get('Content-Disposition');
|
|
68
|
+
const parts = header.split(';');
|
|
69
|
+
const filename = parts[1].split('=')[1];
|
|
70
|
+
const blob = await res.blob();
|
|
71
|
+
await this.downloadBlob(blob, filename);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.error(err);
|
|
75
|
+
this.notificationService.error(err.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
getHeaders() {
|
|
79
|
+
const headers = {};
|
|
80
|
+
const channelToken = this.localStorageService.get('activeChannelToken');
|
|
81
|
+
if (channelToken) {
|
|
82
|
+
headers['vendure-token'] = channelToken;
|
|
83
|
+
}
|
|
84
|
+
const authToken = this.localStorageService.get('authToken');
|
|
85
|
+
if (authToken) {
|
|
86
|
+
headers.authorization = `Bearer ${authToken}`;
|
|
87
|
+
}
|
|
88
|
+
return headers;
|
|
89
|
+
}
|
|
90
|
+
async downloadBlob(blob, fileName) {
|
|
91
|
+
const blobUrl = window.URL.createObjectURL(blob);
|
|
92
|
+
const a = document.createElement('a');
|
|
93
|
+
document.body.appendChild(a);
|
|
94
|
+
a.setAttribute('hidden', 'true');
|
|
95
|
+
a.href = blobUrl;
|
|
96
|
+
a.download = fileName;
|
|
97
|
+
a.setAttribute('target', '_blank');
|
|
98
|
+
a.click();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
ProductExportService = __decorate([
|
|
102
|
+
(0, core_1.Injectable)(),
|
|
103
|
+
__metadata("design:paramtypes", [core_2.DataService,
|
|
104
|
+
core_2.NotificationService,
|
|
105
|
+
core_2.LocalStorageService])
|
|
106
|
+
], ProductExportService);
|
|
107
|
+
exports.ProductExportService = ProductExportService;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core'
|
|
2
|
+
import {
|
|
3
|
+
DataService,
|
|
4
|
+
LocalStorageService,
|
|
5
|
+
NotificationService,
|
|
6
|
+
getServerLocation,
|
|
7
|
+
} from '@vendure/admin-ui/core'
|
|
8
|
+
import { Product } from '@vendure/core'
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class ProductExportService {
|
|
12
|
+
serverPath: string
|
|
13
|
+
constructor(
|
|
14
|
+
protected dataService: DataService,
|
|
15
|
+
private notificationService: NotificationService,
|
|
16
|
+
private localStorageService: LocalStorageService,
|
|
17
|
+
) {
|
|
18
|
+
this.serverPath = getServerLocation()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getCustomFields(productIds: string[]) {
|
|
22
|
+
return fetch(`${this.serverPath}/product-export/custom-fields`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
...this.getHeaders(),
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify(productIds),
|
|
29
|
+
})
|
|
30
|
+
.then((res) => res.json())
|
|
31
|
+
.then((data: { name: string; type: string }[]) => {
|
|
32
|
+
return data.map((field) => field.name)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getConfig() {
|
|
37
|
+
return fetch(`${this.serverPath}/product-export/config`, {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
...this.getHeaders(),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
.then((res) => res.json())
|
|
45
|
+
.then((data) => data)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async exportProducts(
|
|
49
|
+
selection: Product[],
|
|
50
|
+
fileName?: string,
|
|
51
|
+
customFields?: string[],
|
|
52
|
+
exportAssetsAs?: 'url' | 'json',
|
|
53
|
+
) {
|
|
54
|
+
this.notificationService.info(`Exporting ${selection.length} products to ${fileName}.csv`)
|
|
55
|
+
const productIds = selection.map((product) => product.id)
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(
|
|
58
|
+
`${this.serverPath}/product-export/export?fileName=${fileName}&customFields=${customFields}&exportAssetsAs=${exportAssetsAs}`,
|
|
59
|
+
{
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
...this.getHeaders(),
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify(productIds),
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const json = await res.json()
|
|
70
|
+
throw Error(json?.message)
|
|
71
|
+
}
|
|
72
|
+
const header = res.headers.get('Content-Disposition')
|
|
73
|
+
const parts = header!.split(';')
|
|
74
|
+
const filename = parts[1].split('=')[1]
|
|
75
|
+
const blob = await res.blob()
|
|
76
|
+
await this.downloadBlob(blob, filename)
|
|
77
|
+
} catch (err: any) {
|
|
78
|
+
console.error(err)
|
|
79
|
+
this.notificationService.error(err.message)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private getHeaders(): Record<string, string> {
|
|
84
|
+
const headers: Record<string, string> = {}
|
|
85
|
+
const channelToken = this.localStorageService.get('activeChannelToken')
|
|
86
|
+
if (channelToken) {
|
|
87
|
+
headers['vendure-token'] = channelToken
|
|
88
|
+
}
|
|
89
|
+
const authToken = this.localStorageService.get('authToken')
|
|
90
|
+
if (authToken) {
|
|
91
|
+
headers.authorization = `Bearer ${authToken}`
|
|
92
|
+
}
|
|
93
|
+
return headers
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async downloadBlob(blob: Blob, fileName: string): Promise<void> {
|
|
97
|
+
const blobUrl = window.URL.createObjectURL(blob)
|
|
98
|
+
const a = document.createElement('a')
|
|
99
|
+
document.body.appendChild(a)
|
|
100
|
+
a.setAttribute('hidden', 'true')
|
|
101
|
+
a.href = blobUrl
|
|
102
|
+
a.download = fileName
|
|
103
|
+
a.setAttribute('target', '_blank')
|
|
104
|
+
a.click()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@vendure/admin-ui/core");
|
|
4
|
+
const product_export_service_1 = require("./product-export.service");
|
|
5
|
+
const export_dialog_component_1 = require("./export-dialog.component");
|
|
6
|
+
const ngx_translate_extract_marker_1 = require("@biesbjerg/ngx-translate-extract-marker");
|
|
7
|
+
exports.default = [
|
|
8
|
+
(0, core_1.registerBulkAction)({
|
|
9
|
+
location: 'product-list',
|
|
10
|
+
label: (0, ngx_translate_extract_marker_1.marker)('product-export.action'),
|
|
11
|
+
icon: 'export',
|
|
12
|
+
onClick: ({ injector, selection }) => {
|
|
13
|
+
const modalService = injector.get(core_1.ModalService);
|
|
14
|
+
const productExportService = injector.get(product_export_service_1.ProductExportService);
|
|
15
|
+
const productIds = selection.map((product) => product.id);
|
|
16
|
+
const promises = [
|
|
17
|
+
productExportService.getCustomFields(productIds),
|
|
18
|
+
productExportService.getConfig(),
|
|
19
|
+
];
|
|
20
|
+
Promise.all(promises).then(([customFields, config]) => {
|
|
21
|
+
modalService
|
|
22
|
+
.fromComponent(export_dialog_component_1.ExportDialogComponent, {
|
|
23
|
+
size: 'md',
|
|
24
|
+
locals: {
|
|
25
|
+
selection,
|
|
26
|
+
customFields,
|
|
27
|
+
config,
|
|
28
|
+
},
|
|
29
|
+
closable: true,
|
|
30
|
+
})
|
|
31
|
+
.subscribe((response) => {
|
|
32
|
+
if (response?.result) {
|
|
33
|
+
productExportService.exportProducts(selection, response.fileName, response.selectedFields, response.exportAssetsAs);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
product_export_service_1.ProductExportService,
|
|
40
|
+
];
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ModalService, registerBulkAction } from '@vendure/admin-ui/core'
|
|
2
|
+
import { ProductExportService } from './product-export.service'
|
|
3
|
+
import { ExportDialogComponent } from './export-dialog.component'
|
|
4
|
+
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
registerBulkAction({
|
|
8
|
+
location: 'product-list',
|
|
9
|
+
label: _('product-export.action'),
|
|
10
|
+
icon: 'export',
|
|
11
|
+
onClick: ({ injector, selection }) => {
|
|
12
|
+
const modalService = injector.get(ModalService)
|
|
13
|
+
const productExportService = injector.get(ProductExportService)
|
|
14
|
+
|
|
15
|
+
const productIds = selection.map((product) => product.id)
|
|
16
|
+
|
|
17
|
+
const promises = [
|
|
18
|
+
productExportService.getCustomFields(productIds),
|
|
19
|
+
productExportService.getConfig(),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
Promise.all(promises).then(([customFields, config]) => {
|
|
23
|
+
modalService
|
|
24
|
+
.fromComponent(ExportDialogComponent, {
|
|
25
|
+
size: 'md',
|
|
26
|
+
locals: {
|
|
27
|
+
selection,
|
|
28
|
+
customFields,
|
|
29
|
+
config,
|
|
30
|
+
},
|
|
31
|
+
closable: true,
|
|
32
|
+
})
|
|
33
|
+
.subscribe((response) => {
|
|
34
|
+
if (response?.result) {
|
|
35
|
+
productExportService.exportProducts(
|
|
36
|
+
selection,
|
|
37
|
+
response.fileName,
|
|
38
|
+
response.selectedFields,
|
|
39
|
+
response.exportAssetsAs,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
ProductExportService,
|
|
47
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"product-export": {
|
|
3
|
+
"action": "Export products to CSV",
|
|
4
|
+
"dialog": {
|
|
5
|
+
"title": "Export products to CSV",
|
|
6
|
+
"label": "File name (optional)",
|
|
7
|
+
"export": "Export",
|
|
8
|
+
"cancel": "Cancel",
|
|
9
|
+
"customFieldsLabel": "Custom fields to include in the export",
|
|
10
|
+
"exportAssetsAs": "Export assets as: ",
|
|
11
|
+
"exportAssetsAsOptions": {
|
|
12
|
+
"url": "URL",
|
|
13
|
+
"json": "JSON"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"product-export": {
|
|
3
|
+
"action": "Exportera produkter till CSV",
|
|
4
|
+
"dialog": {
|
|
5
|
+
"title": "Exportera produkter till CSV",
|
|
6
|
+
"label": "Filnamn (valfritt)",
|
|
7
|
+
"export": "Exportera",
|
|
8
|
+
"cancel": "Avbryt",
|
|
9
|
+
"customFieldsLabel": "Anpassade fält som ska inkluderas i exporten",
|
|
10
|
+
"exportAssetsAs": "Exportera filer som: ",
|
|
11
|
+
"exportAssetsAsOptions": {
|
|
12
|
+
"url": "URL",
|
|
13
|
+
"json": "JSON"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haus-tech/product-export-plugin",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "A Vendure plugin for importing products from a CSV file",
|
|
5
|
+
"author": "Haus Tech",
|
|
6
|
+
"repository": "https://github.com/WeAreHausTech/haus-tech-vendure-plugins",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "yarn ts-node test/dev-server.ts",
|
|
19
|
+
"build": "yarn run -T rimraf dist && yarn run -T tsc && yarn run -T copyfiles -u 1 'src/ui/**/*' dist/",
|
|
20
|
+
"test": "jest --preset=\"ts-jest\"",
|
|
21
|
+
"prepublishOnly": "yarn && yarn build"
|
|
22
|
+
}
|
|
23
|
+
}
|