@tunghtml/strapi-plugin-export-import-clsx 1.0.2 → 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 +3 -3
- package/server/controllers/export-controller.js +2 -2
- package/server/controllers/import-controller.js +14 -12
- package/server/services/export-service.js +251 -192
- package/server/services/import-service.js +355 -266
- 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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Button } from "@strapi/design-system";
|
|
3
|
+
import { Download } from "@strapi/icons";
|
|
4
|
+
import { useNotification } from "@strapi/strapi/admin";
|
|
5
|
+
|
|
6
|
+
const ExportButton = ({ layout, modifiedData }) => {
|
|
7
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
8
|
+
const { toggleNotification } = useNotification();
|
|
9
|
+
|
|
10
|
+
const handleExport = async () => {
|
|
11
|
+
try {
|
|
12
|
+
const contentType = layout.uid;
|
|
13
|
+
const entryId = modifiedData.id;
|
|
14
|
+
|
|
15
|
+
if (!entryId) {
|
|
16
|
+
toggleNotification({
|
|
17
|
+
type: "warning",
|
|
18
|
+
message: "Please save the entry first",
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setIsExporting(true);
|
|
24
|
+
|
|
25
|
+
const response = await fetch(
|
|
26
|
+
`/export-import-clsx/export/${contentType}/${entryId}`
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (response.ok) {
|
|
30
|
+
const blob = await response.blob();
|
|
31
|
+
const url = window.URL.createObjectURL(blob);
|
|
32
|
+
const a = document.createElement("a");
|
|
33
|
+
a.href = url;
|
|
34
|
+
a.download = `entry-${entryId}-${new Date().toISOString().split("T")[0]}.xlsx`;
|
|
35
|
+
document.body.appendChild(a);
|
|
36
|
+
a.click();
|
|
37
|
+
window.URL.revokeObjectURL(url);
|
|
38
|
+
document.body.removeChild(a);
|
|
39
|
+
|
|
40
|
+
toggleNotification({
|
|
41
|
+
type: "success",
|
|
42
|
+
message: "Entry exported successfully",
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error("Export failed");
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
toggleNotification({
|
|
49
|
+
type: "danger",
|
|
50
|
+
message: `Export failed: ${error.message}`,
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
setIsExporting(false);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Button
|
|
59
|
+
onClick={handleExport}
|
|
60
|
+
loading={isExporting}
|
|
61
|
+
startIcon={<Download />}
|
|
62
|
+
variant="secondary"
|
|
63
|
+
size="S"
|
|
64
|
+
style={{ marginLeft: "8px" }}
|
|
65
|
+
>
|
|
66
|
+
Export Entry
|
|
67
|
+
</Button>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default ExportButton;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
|
+
import { Button } from "@strapi/design-system";
|
|
3
|
+
import { Download, Upload } from "@strapi/icons";
|
|
4
|
+
import { useNotification } from "@strapi/strapi/admin";
|
|
5
|
+
|
|
6
|
+
const ExportImportButtons = (props) => {
|
|
7
|
+
const [isExporting, setIsExporting] = useState(false);
|
|
8
|
+
const [isImporting, setIsImporting] = useState(false);
|
|
9
|
+
const { toggleNotification } = useNotification();
|
|
10
|
+
|
|
11
|
+
// Get current content type from props or URL
|
|
12
|
+
const getContentType = () => {
|
|
13
|
+
if (props.layout?.uid) {
|
|
14
|
+
return props.layout.uid;
|
|
15
|
+
}
|
|
16
|
+
// Fallback: extract from URL - handle both content-manager and event-manager
|
|
17
|
+
const path = window.location.pathname;
|
|
18
|
+
|
|
19
|
+
// For event-manager plugin
|
|
20
|
+
const eventManagerMatch = path.match(
|
|
21
|
+
/\/admin\/plugins\/event-manager\/([^\/]+)\/([^\/]+)/
|
|
22
|
+
);
|
|
23
|
+
if (eventManagerMatch) {
|
|
24
|
+
return eventManagerMatch[2]; // Return the collectionType, not the eventId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// For content-manager
|
|
28
|
+
const contentManagerMatch = path.match(
|
|
29
|
+
/\/admin\/content-manager\/collection-types\/([^\/]+)/
|
|
30
|
+
);
|
|
31
|
+
if (contentManagerMatch) {
|
|
32
|
+
return contentManagerMatch[1];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Get event filter for event manager - simplified with exclude list
|
|
39
|
+
const getEventFilter = () => {
|
|
40
|
+
const path = window.location.pathname;
|
|
41
|
+
const eventManagerMatch = path.match(
|
|
42
|
+
/\/admin\/plugins\/event-manager\/([^\/]+)\/([^\/]+)/
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (eventManagerMatch) {
|
|
46
|
+
const eventId = eventManagerMatch[1];
|
|
47
|
+
const collectionType = eventManagerMatch[2];
|
|
48
|
+
|
|
49
|
+
// Exclude list - content types that don't need event filtering
|
|
50
|
+
const excludeFromEventFilter = [
|
|
51
|
+
"api::audit-log.audit-log",
|
|
52
|
+
"api::business-sector.business-sector",
|
|
53
|
+
"api::email-template.email-template",
|
|
54
|
+
"api::sales-person.sales-person",
|
|
55
|
+
"api::speaker.speaker",
|
|
56
|
+
// Add other content types that are not event-specific
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
eventId &&
|
|
61
|
+
eventId !== "events" &&
|
|
62
|
+
!excludeFromEventFilter.includes(collectionType)
|
|
63
|
+
) {
|
|
64
|
+
// Default to 'event' as relation field name (most common)
|
|
65
|
+
return {
|
|
66
|
+
eventId,
|
|
67
|
+
relationField: "event",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Get current filters from URL
|
|
76
|
+
const getCurrentFilters = () => {
|
|
77
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
78
|
+
const filters = {};
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of urlParams.entries()) {
|
|
81
|
+
if (
|
|
82
|
+
key.startsWith("filters[") ||
|
|
83
|
+
key === "sort" ||
|
|
84
|
+
key === "page" ||
|
|
85
|
+
key === "pageSize" ||
|
|
86
|
+
key === "locale" ||
|
|
87
|
+
key === "_q"
|
|
88
|
+
) {
|
|
89
|
+
filters[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return filters;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Get selected entries from props
|
|
97
|
+
const getSelectedEntries = () => {
|
|
98
|
+
// Try to get selected entries from various possible props
|
|
99
|
+
if (props.selectedEntries && props.selectedEntries.length > 0) {
|
|
100
|
+
return props.selectedEntries;
|
|
101
|
+
}
|
|
102
|
+
if (props.selected && props.selected.length > 0) {
|
|
103
|
+
return props.selected;
|
|
104
|
+
}
|
|
105
|
+
if (props.selection && props.selection.length > 0) {
|
|
106
|
+
return props.selection;
|
|
107
|
+
}
|
|
108
|
+
const selectedIds = [];
|
|
109
|
+
let field = "";
|
|
110
|
+
const getHeaderKey = (i) => {
|
|
111
|
+
const el = document.querySelector(
|
|
112
|
+
`thead th:nth-child(${i}) button, thead th:nth-child(${i}) span`
|
|
113
|
+
);
|
|
114
|
+
if (!el) return "";
|
|
115
|
+
const parts = el.textContent.trim().split(/\s+/);
|
|
116
|
+
return parts.pop(); // last word
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const rows = document.querySelectorAll("tbody tr");
|
|
121
|
+
const allowedFields = [
|
|
122
|
+
"id",
|
|
123
|
+
"name",
|
|
124
|
+
"title",
|
|
125
|
+
"tickerCode",
|
|
126
|
+
"fullName",
|
|
127
|
+
"email",
|
|
128
|
+
"businessEmail",
|
|
129
|
+
"telephone",
|
|
130
|
+
"mobile",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
let foundIndex = null;
|
|
134
|
+
|
|
135
|
+
for (let i = 1; i <= 10; i++) {
|
|
136
|
+
const headerBtn = getHeaderKey(i);
|
|
137
|
+
if (headerBtn !== "" && allowedFields.includes(headerBtn)) {
|
|
138
|
+
field = headerBtn;
|
|
139
|
+
foundIndex = i;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!foundIndex) {
|
|
145
|
+
console.warn("No valid header column found");
|
|
146
|
+
return [[], ""];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// gather values for selected rows
|
|
150
|
+
rows.forEach((row) => {
|
|
151
|
+
const checkbox = row.querySelector(
|
|
152
|
+
'td:nth-child(1) button[role="checkbox"]'
|
|
153
|
+
);
|
|
154
|
+
if (checkbox?.getAttribute("aria-checked") === "true") {
|
|
155
|
+
const cellSpan = row.querySelector(
|
|
156
|
+
`td:nth-child(${foundIndex}) span`
|
|
157
|
+
);
|
|
158
|
+
const text = cellSpan?.textContent.trim();
|
|
159
|
+
if (text) selectedIds.push(text);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return [selectedIds, field];
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error(e);
|
|
166
|
+
return [[], ""];
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleExport = async () => {
|
|
171
|
+
const contentType = getContentType();
|
|
172
|
+
if (!contentType) {
|
|
173
|
+
toggleNotification({
|
|
174
|
+
type: "danger",
|
|
175
|
+
message: "Could not determine content type",
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setIsExporting(true);
|
|
181
|
+
try {
|
|
182
|
+
const filters = getCurrentFilters();
|
|
183
|
+
const eventFilter = getEventFilter(); // Back to sync
|
|
184
|
+
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
185
|
+
|
|
186
|
+
const queryParams = new URLSearchParams({
|
|
187
|
+
format: "excel",
|
|
188
|
+
contentType: contentType,
|
|
189
|
+
...filters,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Add event filter if we're in event manager
|
|
193
|
+
if (eventFilter) {
|
|
194
|
+
queryParams.set(
|
|
195
|
+
`filters[${eventFilter.relationField}][documentId][$eq]`,
|
|
196
|
+
eventFilter.eventId
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Add selected IDs if any
|
|
201
|
+
if (selectedEntries.length > 0) {
|
|
202
|
+
queryParams.set("selectedIds", JSON.stringify(selectedEntries));
|
|
203
|
+
queryParams.set("selectedField", selectedField);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const response = await fetch(`/export-import-clsx/export?${queryParams}`);
|
|
207
|
+
|
|
208
|
+
if (response.ok) {
|
|
209
|
+
const blob = await response.blob();
|
|
210
|
+
const url = window.URL.createObjectURL(blob);
|
|
211
|
+
const a = document.createElement("a");
|
|
212
|
+
a.href = url;
|
|
213
|
+
|
|
214
|
+
// Set filename based on selection
|
|
215
|
+
const filename =
|
|
216
|
+
selectedEntries.length > 0
|
|
217
|
+
? `${contentType.replace("api::", "")}-selected-${selectedEntries.length}-${new Date().toISOString().split("T")[0]}.xlsx`
|
|
218
|
+
: `${contentType.replace("api::", "")}-export-${new Date().toISOString().split("T")[0]}.xlsx`;
|
|
219
|
+
|
|
220
|
+
a.download = filename;
|
|
221
|
+
document.body.appendChild(a);
|
|
222
|
+
a.click();
|
|
223
|
+
window.URL.revokeObjectURL(url);
|
|
224
|
+
document.body.removeChild(a);
|
|
225
|
+
|
|
226
|
+
toggleNotification({
|
|
227
|
+
type: "success",
|
|
228
|
+
message:
|
|
229
|
+
selectedEntries.length > 0
|
|
230
|
+
? `Successfully exported ${selectedEntries.length} selected entries`
|
|
231
|
+
: "Successfully exported data",
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
throw new Error("Export failed");
|
|
235
|
+
}
|
|
236
|
+
} catch (error) {
|
|
237
|
+
toggleNotification({
|
|
238
|
+
type: "danger",
|
|
239
|
+
message: `Export failed: ${error.message}`,
|
|
240
|
+
});
|
|
241
|
+
} finally {
|
|
242
|
+
setIsExporting(false);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const handleImport = async (event) => {
|
|
247
|
+
const file = event.target.files[0];
|
|
248
|
+
if (!file) return;
|
|
249
|
+
|
|
250
|
+
const contentType = getContentType();
|
|
251
|
+
if (!contentType) {
|
|
252
|
+
toggleNotification({
|
|
253
|
+
type: "danger",
|
|
254
|
+
message: "Could not determine content type",
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
setIsImporting(true);
|
|
260
|
+
const formData = new FormData();
|
|
261
|
+
formData.append("file", file);
|
|
262
|
+
formData.append("contentType", contentType);
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch("/export-import-clsx/import", {
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: formData,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (response.ok) {
|
|
271
|
+
const result = await response.json();
|
|
272
|
+
|
|
273
|
+
// Create appropriate notification based on results
|
|
274
|
+
const created = result.summary?.created || result.result.created;
|
|
275
|
+
const updated = result.summary?.updated || result.result.updated;
|
|
276
|
+
const errors = result.result.errors?.length || 0;
|
|
277
|
+
|
|
278
|
+
const total = created + updated;
|
|
279
|
+
|
|
280
|
+
if (errors > 0) {
|
|
281
|
+
toggleNotification({
|
|
282
|
+
type: "warning",
|
|
283
|
+
message: `Import completed with ${errors} error(s). Processed ${total} entries (${created} created, ${updated} updated)`,
|
|
284
|
+
});
|
|
285
|
+
} else if (total > 0) {
|
|
286
|
+
toggleNotification({
|
|
287
|
+
type: "success",
|
|
288
|
+
message: `Import completed successfully! Processed ${total} entries (${created} created, ${updated} updated)`,
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
toggleNotification({
|
|
292
|
+
type: "info",
|
|
293
|
+
message: "Import completed - no changes were made",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Reload the page to show new data
|
|
298
|
+
window.location.reload();
|
|
299
|
+
} else {
|
|
300
|
+
const error = await response.json();
|
|
301
|
+
throw new Error(error.error || "Import failed");
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
toggleNotification({
|
|
305
|
+
type: "danger",
|
|
306
|
+
message: `Import failed: ${error.message}`,
|
|
307
|
+
});
|
|
308
|
+
} finally {
|
|
309
|
+
setIsImporting(false);
|
|
310
|
+
// Reset file input
|
|
311
|
+
if (fileInputRef.current) {
|
|
312
|
+
fileInputRef.current.value = "";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const handleImportClick = () => {
|
|
318
|
+
if (fileInputRef.current) {
|
|
319
|
+
fileInputRef.current.click();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Create ref for file input
|
|
324
|
+
const fileInputRef = useRef(null);
|
|
325
|
+
|
|
326
|
+
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
327
|
+
const exportButtonText =
|
|
328
|
+
selectedEntries.length > 0
|
|
329
|
+
? `Export (${selectedEntries.length})`
|
|
330
|
+
: "Export";
|
|
331
|
+
|
|
332
|
+
return (
|
|
333
|
+
<div
|
|
334
|
+
style={{
|
|
335
|
+
display: "flex",
|
|
336
|
+
gap: "8px",
|
|
337
|
+
alignItems: "center",
|
|
338
|
+
marginRight: "16px",
|
|
339
|
+
order: -1,
|
|
340
|
+
}}
|
|
341
|
+
>
|
|
342
|
+
<Button
|
|
343
|
+
onClick={handleExport}
|
|
344
|
+
loading={isExporting}
|
|
345
|
+
startIcon={<Download />}
|
|
346
|
+
variant="secondary"
|
|
347
|
+
size="S"
|
|
348
|
+
>
|
|
349
|
+
{exportButtonText}
|
|
350
|
+
</Button>
|
|
351
|
+
|
|
352
|
+
<input
|
|
353
|
+
ref={fileInputRef}
|
|
354
|
+
type="file"
|
|
355
|
+
accept=".xlsx,.xls,.json"
|
|
356
|
+
onChange={handleImport}
|
|
357
|
+
disabled={isImporting}
|
|
358
|
+
style={{ display: "none" }}
|
|
359
|
+
/>
|
|
360
|
+
<Button
|
|
361
|
+
onClick={handleImportClick}
|
|
362
|
+
loading={isImporting}
|
|
363
|
+
startIcon={<Upload />}
|
|
364
|
+
variant="secondary"
|
|
365
|
+
size="S"
|
|
366
|
+
disabled={isImporting}
|
|
367
|
+
>
|
|
368
|
+
Import
|
|
369
|
+
</Button>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export default ExportImportButtons;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Button } from "@strapi/design-system";
|
|
3
|
+
import { Upload } from "@strapi/icons";
|
|
4
|
+
import { useNotification } from "@strapi/strapi/admin";
|
|
5
|
+
|
|
6
|
+
const ImportButton = () => {
|
|
7
|
+
const [isImporting, setIsImporting] = useState(false);
|
|
8
|
+
const { toggleNotification } = useNotification();
|
|
9
|
+
|
|
10
|
+
const handleImport = async (event) => {
|
|
11
|
+
const file = event.target.files[0];
|
|
12
|
+
if (!file) return;
|
|
13
|
+
|
|
14
|
+
setIsImporting(true);
|
|
15
|
+
const formData = new FormData();
|
|
16
|
+
formData.append("file", file);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch("/export-import-clsx/import", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
body: formData,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (response.ok) {
|
|
25
|
+
const result = await response.json();
|
|
26
|
+
const imported = result.result.imported || 0;
|
|
27
|
+
const errors = result.result.errors?.length || 0;
|
|
28
|
+
|
|
29
|
+
if (errors > 0) {
|
|
30
|
+
toggleNotification({
|
|
31
|
+
type: "warning",
|
|
32
|
+
message: `Import completed with ${errors} error(s). Imported: ${imported} entries`,
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
toggleNotification({
|
|
36
|
+
type: "success",
|
|
37
|
+
message: `Import completed successfully! Imported: ${imported} entries`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
window.location.reload();
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error("Import failed");
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
toggleNotification({
|
|
47
|
+
type: "danger",
|
|
48
|
+
message: `Import failed: ${error.message}`,
|
|
49
|
+
});
|
|
50
|
+
} finally {
|
|
51
|
+
setIsImporting(false);
|
|
52
|
+
event.target.value = "";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div style={{ display: "inline-block", marginLeft: "8px" }}>
|
|
58
|
+
<input
|
|
59
|
+
type="file"
|
|
60
|
+
accept=".xlsx,.xls,.json"
|
|
61
|
+
onChange={handleImport}
|
|
62
|
+
disabled={isImporting}
|
|
63
|
+
style={{ display: "none" }}
|
|
64
|
+
id="import-file-input"
|
|
65
|
+
/>
|
|
66
|
+
<Button
|
|
67
|
+
as="label"
|
|
68
|
+
htmlFor="import-file-input"
|
|
69
|
+
loading={isImporting}
|
|
70
|
+
startIcon={<Upload />}
|
|
71
|
+
variant="secondary"
|
|
72
|
+
size="S"
|
|
73
|
+
style={{ cursor: isImporting ? "not-allowed" : "pointer" }}
|
|
74
|
+
>
|
|
75
|
+
Import Data
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default ImportButton;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-export-import-clsx",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A powerful Strapi plugin for exporting and importing data with Excel support and advanced filtering",
|
|
5
5
|
"main": "./strapi-server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"strapi-plugin"
|
|
22
22
|
],
|
|
23
23
|
"author": {
|
|
24
|
-
"name": "
|
|
25
|
-
"email": "
|
|
24
|
+
"name": "FinnWasabi",
|
|
25
|
+
"email": "oohlala5533@gmail.com"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"repository": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module.exports = ({ strapi }) => ({
|
|
2
2
|
async export(ctx) {
|
|
3
3
|
try {
|
|
4
|
-
const { format = 'excel', contentType, selectedIds, ...filters } = ctx.query;
|
|
4
|
+
const { format = 'excel', contentType, selectedIds, selectedField, ...filters } = ctx.query;
|
|
5
5
|
const exportService = strapi.plugin('export-import-clsx').service('export-service');
|
|
6
6
|
|
|
7
7
|
// Parse selectedIds if provided
|
|
@@ -15,7 +15,7 @@ module.exports = ({ strapi }) => ({
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (format === 'excel') {
|
|
18
|
-
const buffer = await exportService.exportData('excel', contentType, filters, parsedSelectedIds);
|
|
18
|
+
const buffer = await exportService.exportData('excel', contentType, filters, parsedSelectedIds, selectedField);
|
|
19
19
|
|
|
20
20
|
const filename = parsedSelectedIds.length > 0
|
|
21
21
|
? `${contentType?.replace('api::', '') || 'strapi'}-selected-${parsedSelectedIds.length}-${new Date().toISOString().split('T')[0]}.xlsx`
|
|
@@ -2,24 +2,26 @@ module.exports = ({ strapi }) => ({
|
|
|
2
2
|
async import(ctx) {
|
|
3
3
|
try {
|
|
4
4
|
const { files, body } = ctx.request;
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
if (!files || !files.file) {
|
|
7
|
-
return ctx.throw(400,
|
|
7
|
+
return ctx.throw(400, "No file provided");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const file = Array.isArray(files.file) ? files.file[0] : files.file;
|
|
11
11
|
const targetContentType = body.contentType;
|
|
12
|
-
|
|
13
|
-
const importService = strapi
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
|
|
13
|
+
const importService = strapi
|
|
14
|
+
.plugin("export-import-clsx")
|
|
15
|
+
.service("import-service");
|
|
16
|
+
|
|
17
|
+
const result = await importService.importData(file, targetContentType);
|
|
18
|
+
|
|
17
19
|
// Create appropriate message based on results
|
|
18
|
-
let message =
|
|
20
|
+
let message = "Import completed successfully";
|
|
19
21
|
if (result.errors && result.errors.length > 0) {
|
|
20
22
|
message = `Import completed with ${result.errors.length} error(s). Please check the details below.`;
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
ctx.body = {
|
|
24
26
|
message,
|
|
25
27
|
result,
|
|
@@ -31,12 +33,12 @@ module.exports = ({ strapi }) => ({
|
|
|
31
33
|
},
|
|
32
34
|
};
|
|
33
35
|
} catch (error) {
|
|
34
|
-
strapi.log.error(
|
|
36
|
+
strapi.log.error("Import error:", error);
|
|
35
37
|
ctx.body = {
|
|
36
38
|
error: error.message,
|
|
37
|
-
details: error.stack
|
|
39
|
+
details: error.stack,
|
|
38
40
|
};
|
|
39
41
|
ctx.status = 500;
|
|
40
42
|
}
|
|
41
43
|
},
|
|
42
|
-
});
|
|
44
|
+
});
|