@tunghtml/strapi-plugin-export-import-clsx 1.0.1 → 1.1.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/admin/src/components/ExportButton/index.jsx +71 -0
- package/admin/src/components/ExportImportButtons/index.jsx +374 -0
- package/admin/src/components/ImportButton/index.jsx +81 -0
- package/package.json +4 -4
- package/server/controllers/export-controller.js +2 -2
- package/server/controllers/import-controller.js +13 -11
- package/server/services/export-service.js +304 -213
- package/server/services/import-service.js +409 -268
- package/strapi-admin.js +20 -17
- package/admin/src/components/ExportButton/index.js +0 -48
- package/admin/src/components/ExportImportButtons/index.js +0 -245
- package/admin/src/components/ImportButton/index.js +0 -54
|
@@ -1,19 +1,40 @@
|
|
|
1
|
-
const XLSX = require(
|
|
1
|
+
const XLSX = require("xlsx");
|
|
2
2
|
|
|
3
3
|
module.exports = ({ strapi }) => ({
|
|
4
|
-
async exportData(
|
|
4
|
+
async exportData(
|
|
5
|
+
format = "json",
|
|
6
|
+
contentType = null,
|
|
7
|
+
rawFilters = {},
|
|
8
|
+
selectedIds = [],
|
|
9
|
+
selectedField = null
|
|
10
|
+
) {
|
|
11
|
+
// Normalize content type - handle both content-manager and event-manager formats
|
|
12
|
+
if (contentType && !contentType.startsWith("api::")) {
|
|
13
|
+
// If it's already in api:: format from event-manager, use as is
|
|
14
|
+
// If it's from content-manager, it should already be in correct format
|
|
15
|
+
contentType = contentType;
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
// Get only API content types (collections)
|
|
6
19
|
let contentTypes;
|
|
7
20
|
if (contentType) {
|
|
21
|
+
// Validate that the content type exists
|
|
22
|
+
if (!strapi.contentTypes[contentType]) {
|
|
23
|
+
strapi.log.error(
|
|
24
|
+
`Content type ${contentType} not found. Available types:`,
|
|
25
|
+
Object.keys(strapi.contentTypes)
|
|
26
|
+
);
|
|
27
|
+
throw new Error(`Content type ${contentType} not found`);
|
|
28
|
+
}
|
|
8
29
|
contentTypes = [contentType];
|
|
9
30
|
} else {
|
|
10
|
-
contentTypes = Object.keys(strapi.contentTypes).filter(
|
|
11
|
-
|
|
31
|
+
contentTypes = Object.keys(strapi.contentTypes).filter((key) =>
|
|
32
|
+
key.startsWith("api::")
|
|
12
33
|
);
|
|
13
34
|
}
|
|
14
35
|
|
|
15
36
|
const exportData = {
|
|
16
|
-
version: strapi.config.get(
|
|
37
|
+
version: strapi.config.get("info.strapi"),
|
|
17
38
|
timestamp: new Date().toISOString(),
|
|
18
39
|
data: {},
|
|
19
40
|
};
|
|
@@ -21,108 +42,99 @@ module.exports = ({ strapi }) => ({
|
|
|
21
42
|
for (const ct of contentTypes) {
|
|
22
43
|
try {
|
|
23
44
|
// Parse filters from URL format
|
|
24
|
-
const parsedFilters = this.parseFilters(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
const parsedFilters = this.parseFilters(rawFilters);
|
|
46
|
+
|
|
47
|
+
if (rawFilters["_q"]) {
|
|
48
|
+
parsedFilters._q = rawFilters["_q"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
strapi.log.info(
|
|
52
|
+
`Exporting ${ct} with raw filters: ${JSON.stringify(rawFilters)}`
|
|
53
|
+
);
|
|
54
|
+
strapi.log.info(`Parsed filters: ${JSON.stringify(parsedFilters)}`);
|
|
55
|
+
strapi.log.info(`Selected IDs: ${JSON.stringify(selectedIds)}`);
|
|
29
56
|
|
|
30
57
|
let entries = [];
|
|
31
|
-
|
|
58
|
+
let filters = parsedFilters.filters;
|
|
59
|
+
|
|
32
60
|
// If specific IDs are selected, export only those
|
|
33
61
|
if (selectedIds && selectedIds.length > 0) {
|
|
62
|
+
strapi.log.info(
|
|
63
|
+
`Exporting selected: ${JSON.stringify(selectedIds)}, field: ${selectedField}`
|
|
64
|
+
);
|
|
65
|
+
if (
|
|
66
|
+
selectedField === "id" ||
|
|
67
|
+
(strapi.contentTypes[ct].attributes[selectedField] &&
|
|
68
|
+
["number", "integer", "biginteger", "float", "decimal"].includes(
|
|
69
|
+
strapi.contentTypes[ct].attributes[selectedField].type
|
|
70
|
+
))
|
|
71
|
+
) {
|
|
72
|
+
selectedIds = selectedIds.map((id) => Number(id));
|
|
73
|
+
}
|
|
34
74
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
populate: '*',
|
|
42
|
-
});
|
|
43
|
-
if (entry) {
|
|
44
|
-
entries.push(entry);
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// Fallback for older Strapi versions
|
|
52
|
-
for (const id of selectedIds) {
|
|
53
|
-
try {
|
|
54
|
-
const entry = await strapi.entityService.findOne(ct, id, {
|
|
55
|
-
populate: '*',
|
|
56
|
-
});
|
|
57
|
-
if (entry) {
|
|
58
|
-
entries.push(entry);
|
|
59
|
-
}
|
|
60
|
-
} catch (error) {
|
|
61
|
-
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
75
|
+
entries = await strapi.documents(ct).findMany({
|
|
76
|
+
filters: {
|
|
77
|
+
[selectedField]: { $in: selectedIds },
|
|
78
|
+
},
|
|
79
|
+
populate: "*",
|
|
80
|
+
});
|
|
65
81
|
} catch (error) {
|
|
66
82
|
strapi.log.error(`Failed to export selected entries:`, error);
|
|
67
83
|
}
|
|
68
84
|
} else {
|
|
69
85
|
// Export all entries with filters
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
uniqueEntries.set(docId, entry);
|
|
96
|
-
} else if (!isPublished && !existingIsPublished && isModified && !existingIsModified) {
|
|
97
|
-
uniqueEntries.set(docId, entry);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
entries = Array.from(uniqueEntries.values());
|
|
103
|
-
strapi.log.info(`Found ${allEntries.length} total entries, ${entries.length} unique entries after deduplication`);
|
|
104
|
-
|
|
105
|
-
// Apply filters
|
|
106
|
-
if (parsedFilters && Object.keys(parsedFilters).length > 0) {
|
|
107
|
-
strapi.log.info('Applying filters:', parsedFilters);
|
|
108
|
-
entries = this.applyClientSideFilters(entries, parsedFilters);
|
|
109
|
-
strapi.log.info(`After filtering: ${entries.length} entries`);
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
// Fallback for older Strapi versions
|
|
113
|
-
entries = await strapi.entityService.findMany(ct, {
|
|
114
|
-
populate: '*',
|
|
115
|
-
filters: parsedFilters,
|
|
116
|
-
});
|
|
117
|
-
strapi.log.info(`EntityService found ${entries?.length || 0} entries`);
|
|
86
|
+
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
87
|
+
const numberSearchable = this.getNumberFields(
|
|
88
|
+
strapi.contentTypes[ct]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (parsedFilters._q) {
|
|
92
|
+
strapi.log.info(
|
|
93
|
+
`Applying search query: ${parsedFilters._q} for fields: ${JSON.stringify([...searchable, ...numberSearchable])}`
|
|
94
|
+
);
|
|
95
|
+
const orConditions = [];
|
|
96
|
+
|
|
97
|
+
if (searchable.length > 0) {
|
|
98
|
+
orConditions.push(
|
|
99
|
+
...searchable.map((field) => ({
|
|
100
|
+
[field]: { $containsi: parsedFilters._q },
|
|
101
|
+
}))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
|
|
106
|
+
orConditions.push(
|
|
107
|
+
...numberSearchable.map((field) => ({
|
|
108
|
+
[field]: { $eq: Number(parsedFilters._q) },
|
|
109
|
+
}))
|
|
110
|
+
);
|
|
118
111
|
}
|
|
112
|
+
|
|
113
|
+
if (orConditions.length > 0) {
|
|
114
|
+
filters = {
|
|
115
|
+
...filters,
|
|
116
|
+
$and: [...(filters?.$and || []), { $or: orConditions }],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
strapi.log.info(`Parsed query filters: ${JSON.stringify(filters)}`);
|
|
121
|
+
try {
|
|
122
|
+
entries = await strapi.documents(ct).findMany({
|
|
123
|
+
filters: { ...filters },
|
|
124
|
+
populate: "*",
|
|
125
|
+
});
|
|
126
|
+
strapi.log.info(
|
|
127
|
+
`EntityService found ${entries?.length || 0} entries`
|
|
128
|
+
);
|
|
119
129
|
} catch (error) {
|
|
120
130
|
strapi.log.error(`Failed to query entries:`, error);
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
|
-
|
|
124
|
-
strapi.log.info(
|
|
125
|
-
|
|
133
|
+
|
|
134
|
+
strapi.log.info(
|
|
135
|
+
`Final result: ${entries?.length || 0} entries for ${ct} (total found: ${entries?.length || 0})`
|
|
136
|
+
);
|
|
137
|
+
|
|
126
138
|
if (entries && entries.length > 0) {
|
|
127
139
|
exportData.data[ct] = entries;
|
|
128
140
|
}
|
|
@@ -131,38 +143,89 @@ module.exports = ({ strapi }) => ({
|
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
if (format ===
|
|
146
|
+
if (format === "excel") {
|
|
135
147
|
return this.convertToExcel(exportData.data);
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
return exportData;
|
|
139
151
|
},
|
|
140
152
|
|
|
153
|
+
getSearchableFields(contentTypeSchema) {
|
|
154
|
+
const searchable = [];
|
|
155
|
+
|
|
156
|
+
for (const [fieldName, field] of Object.entries(
|
|
157
|
+
contentTypeSchema.attributes
|
|
158
|
+
)) {
|
|
159
|
+
if (
|
|
160
|
+
["string", "text", "richtext", "email", "uid", "enumeration"].includes(
|
|
161
|
+
field.type
|
|
162
|
+
) &&
|
|
163
|
+
fieldName !== "locale"
|
|
164
|
+
) {
|
|
165
|
+
searchable.push(fieldName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return searchable;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
getNumberFields(contentTypeSchema) {
|
|
173
|
+
const numberFields = [];
|
|
174
|
+
|
|
175
|
+
for (const [fieldName, field] of Object.entries(
|
|
176
|
+
contentTypeSchema.attributes
|
|
177
|
+
)) {
|
|
178
|
+
if (
|
|
179
|
+
["number", "integer", "biginteger", "float", "decimal"].includes(
|
|
180
|
+
field.type
|
|
181
|
+
)
|
|
182
|
+
) {
|
|
183
|
+
numberFields.push(fieldName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
numberFields.push("id");
|
|
188
|
+
|
|
189
|
+
return numberFields;
|
|
190
|
+
},
|
|
191
|
+
|
|
141
192
|
parseFilters(filters) {
|
|
142
193
|
const parsed = {};
|
|
143
|
-
|
|
144
194
|
for (const [key, value] of Object.entries(filters)) {
|
|
145
195
|
// Skip pagination and sorting params
|
|
146
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
[
|
|
198
|
+
"page",
|
|
199
|
+
"pageSize",
|
|
200
|
+
"sort",
|
|
201
|
+
"locale",
|
|
202
|
+
"format",
|
|
203
|
+
"contentType",
|
|
204
|
+
"_q",
|
|
205
|
+
].includes(key)
|
|
206
|
+
) {
|
|
147
207
|
continue;
|
|
148
208
|
}
|
|
149
|
-
|
|
209
|
+
|
|
150
210
|
// Handle URL encoded filter format like filters[$and][0][shortName][$contains]
|
|
151
|
-
if (key.startsWith(
|
|
211
|
+
if (key.startsWith("filters[")) {
|
|
152
212
|
// Extract the actual filter structure
|
|
153
|
-
const match = key.match(
|
|
213
|
+
const match = key.match(
|
|
214
|
+
/filters\[([^\]]+)\](?:\[(\d+)\])?\[([^\]]+)\](?:\[([^\]]+)\])?/
|
|
215
|
+
);
|
|
154
216
|
if (match) {
|
|
155
217
|
const [, operator, index, field, condition] = match;
|
|
156
|
-
|
|
218
|
+
|
|
157
219
|
if (!parsed.filters) parsed.filters = {};
|
|
158
|
-
|
|
159
|
-
if (operator ===
|
|
220
|
+
|
|
221
|
+
if (operator === "$and") {
|
|
160
222
|
if (!parsed.filters.$and) parsed.filters.$and = [];
|
|
161
223
|
const idx = parseInt(index) || 0;
|
|
162
224
|
if (!parsed.filters.$and[idx]) parsed.filters.$and[idx] = {};
|
|
163
|
-
|
|
225
|
+
|
|
164
226
|
if (condition) {
|
|
165
|
-
if (!parsed.filters.$and[idx][field])
|
|
227
|
+
if (!parsed.filters.$and[idx][field])
|
|
228
|
+
parsed.filters.$and[idx][field] = {};
|
|
166
229
|
parsed.filters.$and[idx][field][condition] = value;
|
|
167
230
|
} else {
|
|
168
231
|
parsed.filters.$and[idx][field] = value;
|
|
@@ -173,129 +236,155 @@ module.exports = ({ strapi }) => ({
|
|
|
173
236
|
parsed[key] = value;
|
|
174
237
|
}
|
|
175
238
|
}
|
|
176
|
-
|
|
177
|
-
return parsed;
|
|
178
|
-
},
|
|
179
239
|
|
|
180
|
-
|
|
181
|
-
if (!filters || Object.keys(filters).length === 0) {
|
|
182
|
-
return entries;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const filtered = entries.filter(entry => {
|
|
186
|
-
// Handle structured filters
|
|
187
|
-
if (filters.filters && filters.filters.$and) {
|
|
188
|
-
for (const condition of filters.filters.$and) {
|
|
189
|
-
for (const [field, criteria] of Object.entries(condition)) {
|
|
190
|
-
if (typeof criteria === 'object' && criteria.$contains) {
|
|
191
|
-
// Handle $contains filter
|
|
192
|
-
if (entry[field]) {
|
|
193
|
-
const fieldValue = String(entry[field]).toLowerCase();
|
|
194
|
-
const searchValue = String(criteria.$contains).toLowerCase();
|
|
195
|
-
|
|
196
|
-
if (!fieldValue.includes(searchValue)) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
return false; // Field doesn't exist, exclude entry
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
// Handle exact match
|
|
204
|
-
if (entry[field] !== criteria) {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Handle other filter formats
|
|
213
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
214
|
-
if (key === 'filters') continue; // Already handled above
|
|
215
|
-
|
|
216
|
-
// Handle simple search (global search)
|
|
217
|
-
if (key === '_q' || key === 'search') {
|
|
218
|
-
// Global search across main fields
|
|
219
|
-
const searchFields = ['shortName', 'name', 'title'];
|
|
220
|
-
const searchValue = String(value).toLowerCase();
|
|
221
|
-
|
|
222
|
-
const found = searchFields.some(field => {
|
|
223
|
-
if (entry[field]) {
|
|
224
|
-
return String(entry[field]).toLowerCase().includes(searchValue);
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
if (!found) {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return true;
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
return filtered;
|
|
240
|
+
return parsed;
|
|
239
241
|
},
|
|
240
242
|
|
|
241
243
|
convertToExcel(data) {
|
|
242
244
|
const workbook = XLSX.utils.book_new();
|
|
243
245
|
let hasData = false;
|
|
244
246
|
|
|
247
|
+
const SYSTEM_KEYS = [
|
|
248
|
+
"documentId",
|
|
249
|
+
"locale",
|
|
250
|
+
"createdAt",
|
|
251
|
+
"updatedAt",
|
|
252
|
+
"publishedAt",
|
|
253
|
+
"createdBy",
|
|
254
|
+
"updatedBy",
|
|
255
|
+
"localizations",
|
|
256
|
+
"status",
|
|
257
|
+
];
|
|
258
|
+
const SHORTCUT_FIELDS = [
|
|
259
|
+
"email",
|
|
260
|
+
"businessEmail",
|
|
261
|
+
"name",
|
|
262
|
+
"title",
|
|
263
|
+
"tickerCode",
|
|
264
|
+
];
|
|
265
|
+
|
|
245
266
|
for (const [contentType, entries] of Object.entries(data)) {
|
|
246
267
|
// Clean sheet name (Excel has restrictions)
|
|
247
|
-
const sheetName = contentType
|
|
248
|
-
|
|
268
|
+
const sheetName = contentType
|
|
269
|
+
.split(".")
|
|
270
|
+
.pop()
|
|
271
|
+
.replace(/[^\w\s-]/gi, "_")
|
|
272
|
+
.substring(0, 31);
|
|
273
|
+
|
|
249
274
|
if (entries && entries.length > 0) {
|
|
250
275
|
hasData = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
|
|
277
|
+
const attr = strapi.contentTypes[contentType].attributes;
|
|
278
|
+
const customFields = Object.entries(attr)
|
|
279
|
+
.filter(([key, definition]) => definition.customField)
|
|
280
|
+
.map(([key]) => key);
|
|
281
|
+
|
|
282
|
+
const relationFields = Object.entries(attr)
|
|
283
|
+
.filter(([key, definition]) => definition.type === "relation")
|
|
284
|
+
.map(([key]) => key);
|
|
285
|
+
|
|
286
|
+
const skipFields = Object.entries(attr)
|
|
287
|
+
.filter(([key, definition]) => definition.type === "media")
|
|
288
|
+
.map(([key]) => key);
|
|
289
|
+
|
|
290
|
+
const componentFields = Object.entries(attr)
|
|
291
|
+
.filter(([key, definition]) => definition.type === "component")
|
|
292
|
+
.map(([key]) => key);
|
|
293
|
+
|
|
294
|
+
function handleObject(key, value) {
|
|
295
|
+
if (!value) return;
|
|
296
|
+
if (relationFields.includes(key)) {
|
|
297
|
+
for (const field of SHORTCUT_FIELDS) {
|
|
298
|
+
if (value[field]) {
|
|
299
|
+
return value[field];
|
|
274
300
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
// Clean and flatten entries for Excel
|
|
306
|
+
const cleanedEntries = entries.map((entry) => {
|
|
307
|
+
function cleanAndFlatten(obj) {
|
|
308
|
+
if (Array.isArray(obj)) {
|
|
309
|
+
return obj.map(cleanAndFlatten);
|
|
310
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
311
|
+
const result = {};
|
|
312
|
+
|
|
313
|
+
for (const key in obj) {
|
|
314
|
+
const value = obj[key];
|
|
315
|
+
|
|
316
|
+
// Skip system keys
|
|
317
|
+
if (SYSTEM_KEYS.includes(key)) continue;
|
|
318
|
+
if (customFields.includes(key)) continue;
|
|
319
|
+
if ([...skipFields, "wishlist", "availableSlot"].includes(key))
|
|
320
|
+
continue;
|
|
321
|
+
|
|
322
|
+
if (componentFields.includes(key)) {
|
|
323
|
+
for (const subKey in value) {
|
|
324
|
+
if (subKey === "id") continue;
|
|
325
|
+
result[`${key}_${subKey}`] = value[subKey];
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (value === null || typeof value !== "object") {
|
|
331
|
+
result[key] = value;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!Array.isArray(value) && typeof value === "object") {
|
|
336
|
+
let temp = handleObject(key, value);
|
|
337
|
+
if (temp !== undefined) {
|
|
338
|
+
result[key] = temp;
|
|
339
|
+
}
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
if (value.length > 0 && typeof value[0] === "object") {
|
|
345
|
+
let arrValue = [];
|
|
346
|
+
for (const subValue in value) {
|
|
347
|
+
arrValue.push(handleObject(key, value[subValue]));
|
|
348
|
+
}
|
|
349
|
+
result[key] = arrValue;
|
|
350
|
+
} else {
|
|
351
|
+
result[key] = value;
|
|
352
|
+
}
|
|
279
353
|
continue;
|
|
280
354
|
}
|
|
281
|
-
flatten(obj[key], prefix + key + '_');
|
|
282
|
-
} else if (Array.isArray(obj[key])) {
|
|
283
|
-
flattened[prefix + key] = JSON.stringify(obj[key]);
|
|
284
|
-
} else {
|
|
285
|
-
flattened[prefix + key] = obj[key];
|
|
286
355
|
}
|
|
356
|
+
return result;
|
|
357
|
+
} else {
|
|
358
|
+
return obj; // primitive
|
|
287
359
|
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return
|
|
360
|
+
}
|
|
361
|
+
// Example usage
|
|
362
|
+
const cleaned = cleanAndFlatten(entry);
|
|
363
|
+
return cleaned;
|
|
292
364
|
});
|
|
293
365
|
|
|
294
|
-
|
|
366
|
+
function flattenForXLSX(obj) {
|
|
367
|
+
const result = {};
|
|
368
|
+
for (const key in obj) {
|
|
369
|
+
const value = obj[key];
|
|
370
|
+
if (Array.isArray(value)) {
|
|
371
|
+
result[key] = value.join("|");
|
|
372
|
+
} else {
|
|
373
|
+
result[key] = value;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
const cleanedFlat = cleanedEntries.map((entry) =>
|
|
379
|
+
flattenForXLSX(entry)
|
|
380
|
+
);
|
|
381
|
+
const worksheet = XLSX.utils.json_to_sheet(cleanedFlat);
|
|
295
382
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
296
383
|
} else {
|
|
297
384
|
// Create empty sheet with headers if no data
|
|
298
|
-
const worksheet = XLSX.utils.json_to_sheet([
|
|
385
|
+
const worksheet = XLSX.utils.json_to_sheet([
|
|
386
|
+
{ message: "No data found" },
|
|
387
|
+
]);
|
|
299
388
|
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
|
|
300
389
|
hasData = true; // Prevent empty workbook error
|
|
301
390
|
}
|
|
@@ -303,28 +392,30 @@ module.exports = ({ strapi }) => ({
|
|
|
303
392
|
|
|
304
393
|
// If still no data, create a default sheet
|
|
305
394
|
if (!hasData) {
|
|
306
|
-
const worksheet = XLSX.utils.json_to_sheet([
|
|
307
|
-
|
|
395
|
+
const worksheet = XLSX.utils.json_to_sheet([
|
|
396
|
+
{ message: "No data to export" },
|
|
397
|
+
]);
|
|
398
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, "NoData");
|
|
308
399
|
}
|
|
309
400
|
|
|
310
|
-
return XLSX.write(workbook, { type:
|
|
401
|
+
return XLSX.write(workbook, { type: "buffer", bookType: "xlsx" });
|
|
311
402
|
},
|
|
312
403
|
|
|
313
404
|
async exportSingleEntry(contentType, entryId) {
|
|
314
405
|
try {
|
|
315
406
|
const entry = await strapi.entityService.findOne(contentType, entryId, {
|
|
316
|
-
populate:
|
|
407
|
+
populate: "*",
|
|
317
408
|
});
|
|
318
409
|
|
|
319
410
|
if (!entry) {
|
|
320
|
-
throw new Error(
|
|
411
|
+
throw new Error("Entry not found");
|
|
321
412
|
}
|
|
322
413
|
|
|
323
414
|
const exportData = {
|
|
324
|
-
version: strapi.config.get(
|
|
415
|
+
version: strapi.config.get("info.strapi"),
|
|
325
416
|
timestamp: new Date().toISOString(),
|
|
326
417
|
data: {
|
|
327
|
-
[contentType]: [entry]
|
|
418
|
+
[contentType]: [entry],
|
|
328
419
|
},
|
|
329
420
|
};
|
|
330
421
|
|
|
@@ -333,4 +424,4 @@ module.exports = ({ strapi }) => ({
|
|
|
333
424
|
throw error;
|
|
334
425
|
}
|
|
335
426
|
},
|
|
336
|
-
});
|
|
427
|
+
});
|