@tunghtml/strapi-plugin-export-import-clsx 1.1.0 → 1.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/admin/src/components/ExportImportButtons/index.jsx +6 -99
- package/package.json +1 -1
- package/server/controllers/export-controller.js +56 -43
- package/server/services/export-service.js +48 -74
- package/strapi-admin.js +5 -67
- package/admin/src/components/BulkActions/index.js +0 -70
- package/admin/src/components/ExportButton/index.jsx +0 -71
- package/admin/src/components/ImportButton/index.jsx +0 -81
- package/admin/src/translations/en.json +0 -14
|
@@ -93,80 +93,6 @@ const ExportImportButtons = (props) => {
|
|
|
93
93
|
return filters;
|
|
94
94
|
};
|
|
95
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
96
|
const handleExport = async () => {
|
|
171
97
|
const contentType = getContentType();
|
|
172
98
|
if (!contentType) {
|
|
@@ -180,8 +106,7 @@ const ExportImportButtons = (props) => {
|
|
|
180
106
|
setIsExporting(true);
|
|
181
107
|
try {
|
|
182
108
|
const filters = getCurrentFilters();
|
|
183
|
-
const eventFilter = getEventFilter();
|
|
184
|
-
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
109
|
+
const eventFilter = getEventFilter();
|
|
185
110
|
|
|
186
111
|
const queryParams = new URLSearchParams({
|
|
187
112
|
format: "excel",
|
|
@@ -197,12 +122,6 @@ const ExportImportButtons = (props) => {
|
|
|
197
122
|
);
|
|
198
123
|
}
|
|
199
124
|
|
|
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
125
|
const response = await fetch(`/export-import-clsx/export?${queryParams}`);
|
|
207
126
|
|
|
208
127
|
if (response.ok) {
|
|
@@ -211,11 +130,9 @@ const ExportImportButtons = (props) => {
|
|
|
211
130
|
const a = document.createElement("a");
|
|
212
131
|
a.href = url;
|
|
213
132
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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`;
|
|
133
|
+
const filename = `${contentType.replace("api::", "")}-export-${
|
|
134
|
+
new Date().toISOString().split("T")[0]
|
|
135
|
+
}.xlsx`;
|
|
219
136
|
|
|
220
137
|
a.download = filename;
|
|
221
138
|
document.body.appendChild(a);
|
|
@@ -225,10 +142,7 @@ const ExportImportButtons = (props) => {
|
|
|
225
142
|
|
|
226
143
|
toggleNotification({
|
|
227
144
|
type: "success",
|
|
228
|
-
message:
|
|
229
|
-
selectedEntries.length > 0
|
|
230
|
-
? `Successfully exported ${selectedEntries.length} selected entries`
|
|
231
|
-
: "Successfully exported data",
|
|
145
|
+
message: "Successfully exported data",
|
|
232
146
|
});
|
|
233
147
|
} else {
|
|
234
148
|
throw new Error("Export failed");
|
|
@@ -323,19 +237,12 @@ const ExportImportButtons = (props) => {
|
|
|
323
237
|
// Create ref for file input
|
|
324
238
|
const fileInputRef = useRef(null);
|
|
325
239
|
|
|
326
|
-
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
327
|
-
const exportButtonText =
|
|
328
|
-
selectedEntries.length > 0
|
|
329
|
-
? `Export (${selectedEntries.length})`
|
|
330
|
-
: "Export";
|
|
331
|
-
|
|
332
240
|
return (
|
|
333
241
|
<div
|
|
334
242
|
style={{
|
|
335
243
|
display: "flex",
|
|
336
244
|
gap: "8px",
|
|
337
245
|
alignItems: "center",
|
|
338
|
-
marginRight: "16px",
|
|
339
246
|
order: -1,
|
|
340
247
|
}}
|
|
341
248
|
>
|
|
@@ -346,7 +253,7 @@ const ExportImportButtons = (props) => {
|
|
|
346
253
|
variant="secondary"
|
|
347
254
|
size="S"
|
|
348
255
|
>
|
|
349
|
-
|
|
256
|
+
Export
|
|
350
257
|
</Button>
|
|
351
258
|
|
|
352
259
|
<input
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-export-import-clsx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": {
|
|
@@ -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
|
+
});
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
const XLSX = require("xlsx");
|
|
2
2
|
|
|
3
3
|
module.exports = ({ strapi }) => ({
|
|
4
|
-
async exportData(
|
|
5
|
-
format = "json",
|
|
6
|
-
contentType = null,
|
|
7
|
-
rawFilters = {},
|
|
8
|
-
selectedIds = [],
|
|
9
|
-
selectedField = null
|
|
10
|
-
) {
|
|
4
|
+
async exportData(format = "json", contentType = null, rawFilters = {}) {
|
|
11
5
|
// Normalize content type - handle both content-manager and event-manager formats
|
|
12
6
|
if (contentType && !contentType.startsWith("api::")) {
|
|
13
7
|
// If it's already in api:: format from event-manager, use as is
|
|
@@ -52,87 +46,67 @@ module.exports = ({ strapi }) => ({
|
|
|
52
46
|
`Exporting ${ct} with raw filters: ${JSON.stringify(rawFilters)}`
|
|
53
47
|
);
|
|
54
48
|
strapi.log.info(`Parsed filters: ${JSON.stringify(parsedFilters)}`);
|
|
55
|
-
strapi.log.info(`Selected IDs: ${JSON.stringify(selectedIds)}`);
|
|
56
49
|
|
|
57
50
|
let entries = [];
|
|
58
51
|
let filters = parsedFilters.filters;
|
|
59
52
|
|
|
60
|
-
//
|
|
61
|
-
|
|
53
|
+
// Export all entries with filters
|
|
54
|
+
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
55
|
+
const numberSearchable = this.getNumberFields(strapi.contentTypes[ct]);
|
|
56
|
+
|
|
57
|
+
if (parsedFilters._q) {
|
|
62
58
|
strapi.log.info(
|
|
63
|
-
`
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
strapi.contentTypes[ct].attributes[selectedField].type
|
|
70
|
-
))
|
|
71
|
-
) {
|
|
72
|
-
selectedIds = selectedIds.map((id) => Number(id));
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
entries = await strapi.documents(ct).findMany({
|
|
76
|
-
filters: {
|
|
77
|
-
[selectedField]: { $in: selectedIds },
|
|
78
|
-
},
|
|
79
|
-
populate: "*",
|
|
80
|
-
});
|
|
81
|
-
} catch (error) {
|
|
82
|
-
strapi.log.error(`Failed to export selected entries:`, error);
|
|
83
|
-
}
|
|
84
|
-
} else {
|
|
85
|
-
// Export all entries with filters
|
|
86
|
-
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
87
|
-
const numberSearchable = this.getNumberFields(
|
|
88
|
-
strapi.contentTypes[ct]
|
|
59
|
+
`Applying search query: ${
|
|
60
|
+
parsedFilters._q
|
|
61
|
+
} for fields: ${JSON.stringify([
|
|
62
|
+
...searchable,
|
|
63
|
+
...numberSearchable,
|
|
64
|
+
])}`
|
|
89
65
|
);
|
|
66
|
+
const orConditions = [];
|
|
90
67
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
68
|
+
if (searchable.length > 0) {
|
|
69
|
+
orConditions.push(
|
|
70
|
+
...searchable.map((field) => ({
|
|
71
|
+
[field]: { $containsi: parsedFilters._q },
|
|
72
|
+
}))
|
|
94
73
|
);
|
|
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
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (orConditions.length > 0) {
|
|
114
|
-
filters = {
|
|
115
|
-
...filters,
|
|
116
|
-
$and: [...(filters?.$and || []), { $or: orConditions }],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
74
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
strapi.log.info(
|
|
127
|
-
`EntityService found ${entries?.length || 0} entries`
|
|
75
|
+
|
|
76
|
+
if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
|
|
77
|
+
orConditions.push(
|
|
78
|
+
...numberSearchable.map((field) => ({
|
|
79
|
+
[field]: { $eq: Number(parsedFilters._q) },
|
|
80
|
+
}))
|
|
128
81
|
);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
strapi.log.error(`Failed to query entries:`, error);
|
|
131
82
|
}
|
|
83
|
+
|
|
84
|
+
if (orConditions.length > 0) {
|
|
85
|
+
filters = {
|
|
86
|
+
...filters,
|
|
87
|
+
$and: [...(filters?.$and || []), { $or: orConditions }],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
strapi.log.info(`Parsed query filters: ${JSON.stringify(filters)}`);
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
entries = await strapi.documents(ct).findMany({
|
|
96
|
+
filters: { ...filters },
|
|
97
|
+
populate: "*",
|
|
98
|
+
});
|
|
99
|
+
strapi.log.info(
|
|
100
|
+
`EntityService found ${entries?.length || 0} entries`
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
strapi.log.error(`Failed to query entries:`, error);
|
|
132
104
|
}
|
|
133
105
|
|
|
134
106
|
strapi.log.info(
|
|
135
|
-
`Final result: ${
|
|
107
|
+
`Final result: ${
|
|
108
|
+
entries?.length || 0
|
|
109
|
+
} entries for ${ct} (total found: ${entries?.length || 0})`
|
|
136
110
|
);
|
|
137
111
|
|
|
138
112
|
if (entries && entries.length > 0) {
|
package/strapi-admin.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import pluginPkg from "./package.json";
|
|
3
2
|
import pluginId from "./admin/src/pluginId";
|
|
4
3
|
import Initializer from "./admin/src/components/Initializer";
|
|
@@ -19,73 +18,12 @@ export default {
|
|
|
19
18
|
},
|
|
20
19
|
|
|
21
20
|
bootstrap(app) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
name: "export-import-buttons",
|
|
28
|
-
Component: ExportImportButtons,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
// Method 2: Plugin-based injection
|
|
32
|
-
else if (app.getPlugin) {
|
|
33
|
-
const contentManager = app.getPlugin("content-manager");
|
|
34
|
-
if (contentManager && contentManager.injectComponent) {
|
|
35
|
-
contentManager.injectComponent("listView", "actions", {
|
|
36
|
-
name: "export-import-buttons",
|
|
37
|
-
Component: ExportImportButtons,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Method 3: Global injection
|
|
42
|
-
else if (app.addComponent) {
|
|
43
|
-
app.addComponent(
|
|
44
|
-
"content-manager.listView.actions",
|
|
45
|
-
ExportImportButtons
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.warn("Failed to inject export-import buttons:", error);
|
|
50
|
-
|
|
51
|
-
// Fallback: Add as menu item if injection fails
|
|
52
|
-
app.addMenuLink({
|
|
53
|
-
to: `/plugins/${pluginId}`,
|
|
54
|
-
icon: () => React.createElement("span", null, "📊"),
|
|
55
|
-
intlLabel: {
|
|
56
|
-
id: `${pluginId}.plugin.name`,
|
|
57
|
-
defaultMessage: "Export Import",
|
|
58
|
-
},
|
|
59
|
-
Component: async () => {
|
|
60
|
-
const component = await import("./admin/src/pages/App");
|
|
61
|
-
return component;
|
|
62
|
-
},
|
|
63
|
-
permissions: [],
|
|
21
|
+
const contentManager = app.getPlugin("content-manager");
|
|
22
|
+
if (contentManager && contentManager.injectComponent) {
|
|
23
|
+
contentManager.injectComponent("listView", "actions", {
|
|
24
|
+
name: "export-import-buttons",
|
|
25
|
+
Component: ExportImportButtons,
|
|
64
26
|
});
|
|
65
27
|
}
|
|
66
28
|
},
|
|
67
|
-
|
|
68
|
-
async registerTrads(app) {
|
|
69
|
-
const { locales } = app;
|
|
70
|
-
|
|
71
|
-
const importedTrads = await Promise.all(
|
|
72
|
-
locales.map((locale) => {
|
|
73
|
-
return import(`./admin/src/translations/${locale}.json`)
|
|
74
|
-
.then(({ default: data }) => {
|
|
75
|
-
return {
|
|
76
|
-
data: data,
|
|
77
|
-
locale,
|
|
78
|
-
};
|
|
79
|
-
})
|
|
80
|
-
.catch(() => {
|
|
81
|
-
return {
|
|
82
|
-
data: {},
|
|
83
|
-
locale,
|
|
84
|
-
};
|
|
85
|
-
});
|
|
86
|
-
})
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
return Promise.resolve(importedTrads);
|
|
90
|
-
},
|
|
91
29
|
};
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import ExportButton from '../ExportButton';
|
|
3
|
-
import ImportButton from '../ImportButton';
|
|
4
|
-
|
|
5
|
-
const BulkActions = ({ layout }) => {
|
|
6
|
-
const handleExportAll = async () => {
|
|
7
|
-
try {
|
|
8
|
-
const contentType = layout.uid;
|
|
9
|
-
|
|
10
|
-
// Get current filters from URL if any
|
|
11
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
12
|
-
const filters = {};
|
|
13
|
-
|
|
14
|
-
// Build filters from URL params
|
|
15
|
-
for (const [key, value] of urlParams.entries()) {
|
|
16
|
-
if (key.startsWith('filters[')) {
|
|
17
|
-
filters[key] = value;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const queryString = new URLSearchParams({
|
|
22
|
-
format: 'excel',
|
|
23
|
-
contentType: contentType,
|
|
24
|
-
...filters
|
|
25
|
-
}).toString();
|
|
26
|
-
|
|
27
|
-
const response = await fetch(`/export-import-clsx/export?${queryString}`);
|
|
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 = `${contentType.replace('api::', '')}-export-${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
|
-
} else {
|
|
40
|
-
throw new Error('Export failed');
|
|
41
|
-
}
|
|
42
|
-
} catch (error) {
|
|
43
|
-
alert('Export failed: ' + error.message);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return React.createElement('div', {
|
|
48
|
-
style: {
|
|
49
|
-
display: 'flex',
|
|
50
|
-
gap: '8px',
|
|
51
|
-
alignItems: 'center',
|
|
52
|
-
marginLeft: '16px'
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
React.createElement('button', {
|
|
56
|
-
onClick: handleExportAll,
|
|
57
|
-
style: {
|
|
58
|
-
padding: '8px 16px',
|
|
59
|
-
backgroundColor: '#4945ff',
|
|
60
|
-
color: 'white',
|
|
61
|
-
border: 'none',
|
|
62
|
-
borderRadius: '4px',
|
|
63
|
-
cursor: 'pointer'
|
|
64
|
-
}
|
|
65
|
-
}, 'Export All'),
|
|
66
|
-
React.createElement(ImportButton)
|
|
67
|
-
);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export default BulkActions;
|
|
@@ -1,71 +0,0 @@
|
|
|
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;
|
|
@@ -1,81 +0,0 @@
|
|
|
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;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"plugin.name": "Export Import CLSX",
|
|
3
|
-
"plugin.description": "Export and import data with enhanced functionality",
|
|
4
|
-
"export.title": "Export Data",
|
|
5
|
-
"export.description": "Export all your content types data to a JSON file",
|
|
6
|
-
"export.button": "Export Data",
|
|
7
|
-
"export.success": "Export completed successfully",
|
|
8
|
-
"export.error": "Export failed",
|
|
9
|
-
"import.title": "Import Data",
|
|
10
|
-
"import.description": "Import data from a JSON file to your Strapi instance",
|
|
11
|
-
"import.button": "Import Data",
|
|
12
|
-
"import.success": "Import completed successfully",
|
|
13
|
-
"import.error": "Import failed"
|
|
14
|
-
}
|