@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.
@@ -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.1",
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": "finnwasabi",
25
- "email": "finnwasabi@example.com"
24
+ "name": "FinnWasabi",
25
+ "email": "oohlala5533@gmail.com"
26
26
  },
27
27
  "license": "MIT",
28
28
  "repository": {
@@ -57,4 +57,4 @@
57
57
  "kind": "plugin",
58
58
  "category": "data-management"
59
59
  }
60
- }
60
+ }
@@ -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, 'No file provided');
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.plugin('export-import-clsx').service('import-service');
14
-
12
+
13
+ const importService = strapi
14
+ .plugin("export-import-clsx")
15
+ .service("import-service");
16
+
15
17
  const result = await importService.importData(file, targetContentType);
16
-
18
+
17
19
  // Create appropriate message based on results
18
- let message = 'Import completed successfully';
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('Import error:', 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
+ });