@nkhang1902/strapi-plugin-export-import-clsx 1.0.5 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/components/ExportImportButtons/index.jsx +281 -0
- package/package.json +3 -3
- package/server/controllers/export-controller.js +56 -43
- package/server/controllers/import-controller.js +14 -12
- package/server/services/export-service.js +160 -116
- package/server/services/import-service.js +382 -344
- package/strapi-admin.js +10 -69
- package/admin/src/components/BulkActions/index.js +0 -70
- package/admin/src/components/ExportButton/index.js +0 -48
- package/admin/src/components/ExportImportButtons/index.js +0 -279
- package/admin/src/components/ImportButton/index.js +0 -54
- package/admin/src/translations/en.json +0 -14
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
const handleExport = async () => {
|
|
97
|
+
const contentType = getContentType();
|
|
98
|
+
if (!contentType) {
|
|
99
|
+
toggleNotification({
|
|
100
|
+
type: "danger",
|
|
101
|
+
message: "Could not determine content type",
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setIsExporting(true);
|
|
107
|
+
try {
|
|
108
|
+
const filters = getCurrentFilters();
|
|
109
|
+
const eventFilter = getEventFilter();
|
|
110
|
+
|
|
111
|
+
const queryParams = new URLSearchParams({
|
|
112
|
+
format: "excel",
|
|
113
|
+
contentType: contentType,
|
|
114
|
+
...filters,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Add event filter if we're in event manager
|
|
118
|
+
if (eventFilter) {
|
|
119
|
+
queryParams.set(
|
|
120
|
+
`filters[${eventFilter.relationField}][documentId][$eq]`,
|
|
121
|
+
eventFilter.eventId
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await fetch(`/export-import-clsx/export?${queryParams}`);
|
|
126
|
+
|
|
127
|
+
if (response.ok) {
|
|
128
|
+
const blob = await response.blob();
|
|
129
|
+
const url = window.URL.createObjectURL(blob);
|
|
130
|
+
const a = document.createElement("a");
|
|
131
|
+
a.href = url;
|
|
132
|
+
|
|
133
|
+
const filename = `${contentType.replace("api::", "")}-export-${
|
|
134
|
+
new Date().toISOString().split("T")[0]
|
|
135
|
+
}.xlsx`;
|
|
136
|
+
|
|
137
|
+
a.download = filename;
|
|
138
|
+
document.body.appendChild(a);
|
|
139
|
+
a.click();
|
|
140
|
+
window.URL.revokeObjectURL(url);
|
|
141
|
+
document.body.removeChild(a);
|
|
142
|
+
|
|
143
|
+
toggleNotification({
|
|
144
|
+
type: "success",
|
|
145
|
+
message: "Successfully exported data",
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
throw new Error("Export failed");
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
toggleNotification({
|
|
152
|
+
type: "danger",
|
|
153
|
+
message: `Export failed: ${error.message}`,
|
|
154
|
+
});
|
|
155
|
+
} finally {
|
|
156
|
+
setIsExporting(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleImport = async (event) => {
|
|
161
|
+
const file = event.target.files[0];
|
|
162
|
+
if (!file) return;
|
|
163
|
+
|
|
164
|
+
const contentType = getContentType();
|
|
165
|
+
if (!contentType) {
|
|
166
|
+
toggleNotification({
|
|
167
|
+
type: "danger",
|
|
168
|
+
message: "Could not determine content type",
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setIsImporting(true);
|
|
174
|
+
const formData = new FormData();
|
|
175
|
+
formData.append("file", file);
|
|
176
|
+
formData.append("contentType", contentType);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch("/export-import-clsx/import", {
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: formData,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (response.ok) {
|
|
185
|
+
const result = await response.json();
|
|
186
|
+
|
|
187
|
+
// Create appropriate notification based on results
|
|
188
|
+
const created = result.summary?.created || result.result.created;
|
|
189
|
+
const updated = result.summary?.updated || result.result.updated;
|
|
190
|
+
const errors = result.result.errors?.length || 0;
|
|
191
|
+
|
|
192
|
+
const total = created + updated;
|
|
193
|
+
|
|
194
|
+
if (errors > 0) {
|
|
195
|
+
toggleNotification({
|
|
196
|
+
type: "warning",
|
|
197
|
+
message: `Import completed with ${errors} error(s). Processed ${total} entries (${created} created, ${updated} updated)`,
|
|
198
|
+
});
|
|
199
|
+
} else if (total > 0) {
|
|
200
|
+
toggleNotification({
|
|
201
|
+
type: "success",
|
|
202
|
+
message: `Import completed successfully! Processed ${total} entries (${created} created, ${updated} updated)`,
|
|
203
|
+
});
|
|
204
|
+
} else {
|
|
205
|
+
toggleNotification({
|
|
206
|
+
type: "info",
|
|
207
|
+
message: "Import completed - no changes were made",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Reload the page to show new data
|
|
212
|
+
window.location.reload();
|
|
213
|
+
} else {
|
|
214
|
+
const error = await response.json();
|
|
215
|
+
throw new Error(error.error || "Import failed");
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
toggleNotification({
|
|
219
|
+
type: "danger",
|
|
220
|
+
message: `Import failed: ${error.message}`,
|
|
221
|
+
});
|
|
222
|
+
} finally {
|
|
223
|
+
setIsImporting(false);
|
|
224
|
+
// Reset file input
|
|
225
|
+
if (fileInputRef.current) {
|
|
226
|
+
fileInputRef.current.value = "";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleImportClick = () => {
|
|
232
|
+
if (fileInputRef.current) {
|
|
233
|
+
fileInputRef.current.click();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Create ref for file input
|
|
238
|
+
const fileInputRef = useRef(null);
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
style={{
|
|
243
|
+
display: "flex",
|
|
244
|
+
gap: "8px",
|
|
245
|
+
alignItems: "center",
|
|
246
|
+
order: -1,
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
<Button
|
|
250
|
+
onClick={handleExport}
|
|
251
|
+
loading={isExporting}
|
|
252
|
+
startIcon={<Download />}
|
|
253
|
+
variant="secondary"
|
|
254
|
+
size="S"
|
|
255
|
+
>
|
|
256
|
+
Export
|
|
257
|
+
</Button>
|
|
258
|
+
|
|
259
|
+
<input
|
|
260
|
+
ref={fileInputRef}
|
|
261
|
+
type="file"
|
|
262
|
+
accept=".xlsx,.xls,.json"
|
|
263
|
+
onChange={handleImport}
|
|
264
|
+
disabled={isImporting}
|
|
265
|
+
style={{ display: "none" }}
|
|
266
|
+
/>
|
|
267
|
+
<Button
|
|
268
|
+
onClick={handleImportClick}
|
|
269
|
+
loading={isImporting}
|
|
270
|
+
startIcon={<Upload />}
|
|
271
|
+
variant="secondary"
|
|
272
|
+
size="S"
|
|
273
|
+
disabled={isImporting}
|
|
274
|
+
>
|
|
275
|
+
Import
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export default ExportImportButtons;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkhang1902/strapi-plugin-export-import-clsx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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,62 +1,75 @@
|
|
|
1
1
|
module.exports = ({ strapi }) => ({
|
|
2
2
|
async export(ctx) {
|
|
3
3
|
try {
|
|
4
|
-
const { format =
|
|
5
|
-
const exportService = strapi
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
ctx.set(
|
|
25
|
-
|
|
26
|
-
|
|
4
|
+
const { format = "excel", contentType, ...filters } = ctx.query;
|
|
5
|
+
const exportService = strapi
|
|
6
|
+
.plugin("export-import-clsx")
|
|
7
|
+
.service("export-service");
|
|
8
|
+
|
|
9
|
+
if (format === "excel") {
|
|
10
|
+
const buffer = await exportService.exportData(
|
|
11
|
+
"excel",
|
|
12
|
+
contentType,
|
|
13
|
+
filters
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const filename = `${
|
|
17
|
+
contentType?.replace("api::", "") || "strapi"
|
|
18
|
+
}-export-${new Date().toISOString().split("T")[0]}.xlsx`;
|
|
19
|
+
|
|
20
|
+
ctx.set(
|
|
21
|
+
"Content-Type",
|
|
22
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
23
|
+
);
|
|
24
|
+
ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
|
|
25
|
+
|
|
27
26
|
ctx.body = buffer;
|
|
28
27
|
} else {
|
|
29
|
-
const data = await exportService.exportData(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const data = await exportService.exportData(
|
|
29
|
+
"json",
|
|
30
|
+
contentType,
|
|
31
|
+
filters
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const filename = `${
|
|
35
|
+
contentType?.replace("api::", "") || "strapi"
|
|
36
|
+
}-export-${new Date().toISOString().split("T")[0]}.json`;
|
|
37
|
+
|
|
38
|
+
ctx.set("Content-Type", "application/json");
|
|
39
|
+
ctx.set("Content-Disposition", `attachment; filename="${filename}"`);
|
|
40
|
+
|
|
38
41
|
ctx.body = JSON.stringify(data, null, 2);
|
|
39
42
|
}
|
|
40
43
|
} catch (error) {
|
|
41
|
-
strapi.log.error(
|
|
42
|
-
ctx.throw(500,
|
|
44
|
+
strapi.log.error("Export error:", error);
|
|
45
|
+
ctx.throw(500, "Export failed");
|
|
43
46
|
}
|
|
44
47
|
},
|
|
45
48
|
|
|
46
49
|
async exportSingle(ctx) {
|
|
47
50
|
try {
|
|
48
51
|
const { contentType, id } = ctx.params;
|
|
49
|
-
const exportService = strapi
|
|
50
|
-
|
|
52
|
+
const exportService = strapi
|
|
53
|
+
.plugin("export-import-clsx")
|
|
54
|
+
.service("export-service");
|
|
55
|
+
|
|
51
56
|
const buffer = await exportService.exportSingleEntry(contentType, id);
|
|
52
|
-
|
|
53
|
-
ctx.set(
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
|
|
58
|
+
ctx.set(
|
|
59
|
+
"Content-Type",
|
|
60
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
61
|
+
);
|
|
62
|
+
ctx.set(
|
|
63
|
+
"Content-Disposition",
|
|
64
|
+
`attachment; filename="entry-${id}-${
|
|
65
|
+
new Date().toISOString().split("T")[0]
|
|
66
|
+
}.xlsx"`
|
|
67
|
+
);
|
|
68
|
+
|
|
56
69
|
ctx.body = buffer;
|
|
57
70
|
} catch (error) {
|
|
58
|
-
strapi.log.error(
|
|
59
|
-
ctx.throw(500,
|
|
71
|
+
strapi.log.error("Export single error:", error);
|
|
72
|
+
ctx.throw(500, "Export failed");
|
|
60
73
|
}
|
|
61
74
|
},
|
|
62
|
-
});
|
|
75
|
+
});
|
|
@@ -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
|
+
});
|