@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
package/strapi-admin.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import pluginPkg from
|
|
3
|
-
import pluginId from
|
|
4
|
-
import Initializer from
|
|
5
|
-
import ExportImportButtons from
|
|
1
|
+
import React from "react";
|
|
2
|
+
import pluginPkg from "./package.json";
|
|
3
|
+
import pluginId from "./admin/src/pluginId";
|
|
4
|
+
import Initializer from "./admin/src/components/Initializer";
|
|
5
|
+
import ExportImportButtons from "./admin/src/components/ExportImportButtons";
|
|
6
6
|
|
|
7
7
|
const name = pluginPkg.strapi.name;
|
|
8
8
|
|
|
@@ -23,38 +23,41 @@ export default {
|
|
|
23
23
|
try {
|
|
24
24
|
// Method 1: Direct injection
|
|
25
25
|
if (app.injectContentManagerComponent) {
|
|
26
|
-
app.injectContentManagerComponent(
|
|
27
|
-
name:
|
|
26
|
+
app.injectContentManagerComponent("listView", "actions", {
|
|
27
|
+
name: "export-import-buttons",
|
|
28
28
|
Component: ExportImportButtons,
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
// Method 2: Plugin-based injection
|
|
32
32
|
else if (app.getPlugin) {
|
|
33
|
-
const contentManager = app.getPlugin(
|
|
33
|
+
const contentManager = app.getPlugin("content-manager");
|
|
34
34
|
if (contentManager && contentManager.injectComponent) {
|
|
35
|
-
contentManager.injectComponent(
|
|
36
|
-
name:
|
|
35
|
+
contentManager.injectComponent("listView", "actions", {
|
|
36
|
+
name: "export-import-buttons",
|
|
37
37
|
Component: ExportImportButtons,
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
// Method 3: Global injection
|
|
42
42
|
else if (app.addComponent) {
|
|
43
|
-
app.addComponent(
|
|
43
|
+
app.addComponent(
|
|
44
|
+
"content-manager.listView.actions",
|
|
45
|
+
ExportImportButtons
|
|
46
|
+
);
|
|
44
47
|
}
|
|
45
48
|
} catch (error) {
|
|
46
|
-
console.warn(
|
|
47
|
-
|
|
49
|
+
console.warn("Failed to inject export-import buttons:", error);
|
|
50
|
+
|
|
48
51
|
// Fallback: Add as menu item if injection fails
|
|
49
52
|
app.addMenuLink({
|
|
50
53
|
to: `/plugins/${pluginId}`,
|
|
51
|
-
icon: () => React.createElement(
|
|
54
|
+
icon: () => React.createElement("span", null, "📊"),
|
|
52
55
|
intlLabel: {
|
|
53
56
|
id: `${pluginId}.plugin.name`,
|
|
54
|
-
defaultMessage:
|
|
57
|
+
defaultMessage: "Export Import",
|
|
55
58
|
},
|
|
56
59
|
Component: async () => {
|
|
57
|
-
const component = await import(
|
|
60
|
+
const component = await import("./admin/src/pages/App");
|
|
58
61
|
return component;
|
|
59
62
|
},
|
|
60
63
|
permissions: [],
|
|
@@ -85,4 +88,4 @@ export default {
|
|
|
85
88
|
|
|
86
89
|
return Promise.resolve(importedTrads);
|
|
87
90
|
},
|
|
88
|
-
};
|
|
91
|
+
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
const ExportButton = ({ layout, modifiedData }) => {
|
|
4
|
-
const handleExport = async () => {
|
|
5
|
-
try {
|
|
6
|
-
const contentType = layout.uid;
|
|
7
|
-
const entryId = modifiedData.id;
|
|
8
|
-
|
|
9
|
-
if (!entryId) {
|
|
10
|
-
alert('Please save the entry first');
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const response = await fetch(`/export-import-clsx/export/${contentType}/${entryId}`);
|
|
15
|
-
|
|
16
|
-
if (response.ok) {
|
|
17
|
-
const blob = await response.blob();
|
|
18
|
-
const url = window.URL.createObjectURL(blob);
|
|
19
|
-
const a = document.createElement('a');
|
|
20
|
-
a.href = url;
|
|
21
|
-
a.download = `entry-${entryId}-${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
22
|
-
document.body.appendChild(a);
|
|
23
|
-
a.click();
|
|
24
|
-
window.URL.revokeObjectURL(url);
|
|
25
|
-
document.body.removeChild(a);
|
|
26
|
-
} else {
|
|
27
|
-
throw new Error('Export failed');
|
|
28
|
-
}
|
|
29
|
-
} catch (error) {
|
|
30
|
-
alert('Export failed: ' + error.message);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
return React.createElement('button', {
|
|
35
|
-
onClick: handleExport,
|
|
36
|
-
style: {
|
|
37
|
-
padding: '8px 16px',
|
|
38
|
-
backgroundColor: '#4945ff',
|
|
39
|
-
color: 'white',
|
|
40
|
-
border: 'none',
|
|
41
|
-
borderRadius: '4px',
|
|
42
|
-
cursor: 'pointer',
|
|
43
|
-
marginLeft: '8px'
|
|
44
|
-
}
|
|
45
|
-
}, 'Export Entry');
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export default ExportButton;
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
const ExportImportButtons = (props) => {
|
|
4
|
-
const [isExporting, setIsExporting] = useState(false);
|
|
5
|
-
const [isImporting, setIsImporting] = useState(false);
|
|
6
|
-
|
|
7
|
-
// Get current content type from props or URL
|
|
8
|
-
const getContentType = () => {
|
|
9
|
-
if (props.layout?.uid) {
|
|
10
|
-
return props.layout.uid;
|
|
11
|
-
}
|
|
12
|
-
// Fallback: extract from URL
|
|
13
|
-
const path = window.location.pathname;
|
|
14
|
-
const match = path.match(/\/admin\/content-manager\/collection-types\/([^\/]+)/);
|
|
15
|
-
return match ? match[1] : null;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Get current filters from URL
|
|
19
|
-
const getCurrentFilters = () => {
|
|
20
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
21
|
-
const filters = {};
|
|
22
|
-
|
|
23
|
-
for (const [key, value] of urlParams.entries()) {
|
|
24
|
-
if (key.startsWith('filters[') || key === 'sort' || key === 'page' || key === 'pageSize' || key === 'locale') {
|
|
25
|
-
filters[key] = value;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return filters;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Get selected entries from props
|
|
33
|
-
const getSelectedEntries = () => {
|
|
34
|
-
// Try to get selected entries from various possible props
|
|
35
|
-
if (props.selectedEntries && props.selectedEntries.length > 0) {
|
|
36
|
-
return props.selectedEntries;
|
|
37
|
-
}
|
|
38
|
-
if (props.selected && props.selected.length > 0) {
|
|
39
|
-
return props.selected;
|
|
40
|
-
}
|
|
41
|
-
if (props.selection && props.selection.length > 0) {
|
|
42
|
-
return props.selection;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Try to get from global state or context
|
|
46
|
-
try {
|
|
47
|
-
// Check if there's a selection in the page context
|
|
48
|
-
const checkboxes = document.querySelectorAll('input[type="checkbox"]:checked');
|
|
49
|
-
const selectedIds = [];
|
|
50
|
-
checkboxes.forEach(checkbox => {
|
|
51
|
-
const value = checkbox.value;
|
|
52
|
-
if (value && value !== 'on' && value !== 'all') {
|
|
53
|
-
selectedIds.push(value);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return selectedIds;
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return [];
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const handleExport = async () => {
|
|
63
|
-
const contentType = getContentType();
|
|
64
|
-
if (!contentType) {
|
|
65
|
-
alert('Could not determine content type');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
setIsExporting(true);
|
|
70
|
-
try {
|
|
71
|
-
const filters = getCurrentFilters();
|
|
72
|
-
const selectedEntries = getSelectedEntries();
|
|
73
|
-
|
|
74
|
-
const queryParams = new URLSearchParams({
|
|
75
|
-
format: 'excel',
|
|
76
|
-
contentType: contentType,
|
|
77
|
-
...filters
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Add selected IDs if any
|
|
81
|
-
if (selectedEntries.length > 0) {
|
|
82
|
-
queryParams.set('selectedIds', JSON.stringify(selectedEntries));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const response = await fetch(`/export-import-clsx/export?${queryParams}`);
|
|
86
|
-
|
|
87
|
-
if (response.ok) {
|
|
88
|
-
const blob = await response.blob();
|
|
89
|
-
const url = window.URL.createObjectURL(blob);
|
|
90
|
-
const a = document.createElement('a');
|
|
91
|
-
a.href = url;
|
|
92
|
-
|
|
93
|
-
// Set filename based on selection
|
|
94
|
-
const filename = selectedEntries.length > 0
|
|
95
|
-
? `${contentType.replace('api::', '')}-selected-${selectedEntries.length}-${new Date().toISOString().split('T')[0]}.xlsx`
|
|
96
|
-
: `${contentType.replace('api::', '')}-export-${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
97
|
-
|
|
98
|
-
a.download = filename;
|
|
99
|
-
document.body.appendChild(a);
|
|
100
|
-
a.click();
|
|
101
|
-
window.URL.revokeObjectURL(url);
|
|
102
|
-
document.body.removeChild(a);
|
|
103
|
-
} else {
|
|
104
|
-
throw new Error('Export failed');
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
alert('Export failed: ' + error.message);
|
|
108
|
-
} finally {
|
|
109
|
-
setIsExporting(false);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const handleImport = async (event) => {
|
|
114
|
-
const file = event.target.files[0];
|
|
115
|
-
if (!file) return;
|
|
116
|
-
|
|
117
|
-
const contentType = getContentType();
|
|
118
|
-
if (!contentType) {
|
|
119
|
-
alert('Could not determine content type');
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
setIsImporting(true);
|
|
124
|
-
const formData = new FormData();
|
|
125
|
-
formData.append('file', file);
|
|
126
|
-
formData.append('contentType', contentType);
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const response = await fetch('/export-import-clsx/import', {
|
|
130
|
-
method: 'POST',
|
|
131
|
-
body: formData,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (response.ok) {
|
|
135
|
-
const result = await response.json();
|
|
136
|
-
|
|
137
|
-
// Create simple, human message
|
|
138
|
-
const created = result.summary?.created || result.result.created;
|
|
139
|
-
const updated = result.summary?.updated || result.result.updated;
|
|
140
|
-
const errors = result.result.errors?.length || 0;
|
|
141
|
-
|
|
142
|
-
const total = created + updated;
|
|
143
|
-
let message = 'Import completed!\n\n';
|
|
144
|
-
|
|
145
|
-
if (total > 0) {
|
|
146
|
-
message += `Processed ${total} ${total === 1 ? 'entry' : 'entries'}\n`;
|
|
147
|
-
if (created > 0) {
|
|
148
|
-
message += `• Created: ${created}\n`;
|
|
149
|
-
}
|
|
150
|
-
if (updated > 0) {
|
|
151
|
-
message += `• Updated: ${updated}\n`;
|
|
152
|
-
}
|
|
153
|
-
} else if (errors === 0) {
|
|
154
|
-
message += 'No changes were made\n';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (errors > 0) {
|
|
158
|
-
message += `\nFound ${errors} ${errors === 1 ? 'error' : 'errors'}:\n`;
|
|
159
|
-
result.result.errors.slice(0, 2).forEach((error, index) => {
|
|
160
|
-
message += `• ${error}\n`;
|
|
161
|
-
});
|
|
162
|
-
if (errors > 2) {
|
|
163
|
-
message += `• ... and ${errors - 2} more\n`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
alert(message);
|
|
168
|
-
|
|
169
|
-
// Reload the page to show new data
|
|
170
|
-
window.location.reload();
|
|
171
|
-
} else {
|
|
172
|
-
const error = await response.json();
|
|
173
|
-
throw new Error(error.error || 'Import failed');
|
|
174
|
-
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
alert('Import failed: ' + error.message);
|
|
177
|
-
} finally {
|
|
178
|
-
setIsImporting(false);
|
|
179
|
-
event.target.value = '';
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const selectedEntries = getSelectedEntries();
|
|
184
|
-
const exportButtonText = isExporting
|
|
185
|
-
? 'Exporting...'
|
|
186
|
-
: selectedEntries.length > 0
|
|
187
|
-
? `Export (${selectedEntries.length})`
|
|
188
|
-
: 'Export';
|
|
189
|
-
|
|
190
|
-
return React.createElement('div', {
|
|
191
|
-
style: {
|
|
192
|
-
display: 'flex',
|
|
193
|
-
gap: '8px',
|
|
194
|
-
alignItems: 'center',
|
|
195
|
-
marginRight: '16px',
|
|
196
|
-
order: -1 // This will place it before other elements
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
// Export Button
|
|
200
|
-
React.createElement('button', {
|
|
201
|
-
onClick: handleExport,
|
|
202
|
-
disabled: isExporting,
|
|
203
|
-
style: {
|
|
204
|
-
padding: '8px 16px',
|
|
205
|
-
backgroundColor: isExporting ? '#dcdce4' : '#4945ff',
|
|
206
|
-
color: 'white',
|
|
207
|
-
border: 'none',
|
|
208
|
-
borderRadius: '4px',
|
|
209
|
-
fontSize: '14px',
|
|
210
|
-
fontWeight: '500',
|
|
211
|
-
cursor: isExporting ? 'not-allowed' : 'pointer',
|
|
212
|
-
transition: 'background-color 0.2s'
|
|
213
|
-
}
|
|
214
|
-
}, exportButtonText),
|
|
215
|
-
|
|
216
|
-
// Import Button - same color as Export
|
|
217
|
-
React.createElement('div', { style: { position: 'relative' } },
|
|
218
|
-
React.createElement('input', {
|
|
219
|
-
type: 'file',
|
|
220
|
-
accept: '.xlsx,.xls,.json',
|
|
221
|
-
onChange: handleImport,
|
|
222
|
-
disabled: isImporting,
|
|
223
|
-
style: { display: 'none' },
|
|
224
|
-
id: 'import-file-input'
|
|
225
|
-
}),
|
|
226
|
-
React.createElement('label', {
|
|
227
|
-
htmlFor: 'import-file-input',
|
|
228
|
-
style: {
|
|
229
|
-
display: 'inline-block',
|
|
230
|
-
padding: '8px 16px',
|
|
231
|
-
backgroundColor: isImporting ? '#dcdce4' : '#4945ff', // Same color as Export
|
|
232
|
-
color: 'white',
|
|
233
|
-
border: 'none',
|
|
234
|
-
borderRadius: '4px',
|
|
235
|
-
fontSize: '14px',
|
|
236
|
-
fontWeight: '500',
|
|
237
|
-
cursor: isImporting ? 'not-allowed' : 'pointer',
|
|
238
|
-
transition: 'background-color 0.2s'
|
|
239
|
-
}
|
|
240
|
-
}, isImporting ? 'Importing...' : 'Import')
|
|
241
|
-
)
|
|
242
|
-
);
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
export default ExportImportButtons;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
const ImportButton = () => {
|
|
4
|
-
const handleImport = async (event) => {
|
|
5
|
-
const file = event.target.files[0];
|
|
6
|
-
if (!file) return;
|
|
7
|
-
|
|
8
|
-
const formData = new FormData();
|
|
9
|
-
formData.append('file', file);
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const response = await fetch('/export-import-clsx/import', {
|
|
13
|
-
method: 'POST',
|
|
14
|
-
body: formData,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
if (response.ok) {
|
|
18
|
-
const result = await response.json();
|
|
19
|
-
alert(`Import completed! Imported: ${result.result.imported}, Errors: ${result.result.errors.length}`);
|
|
20
|
-
window.location.reload();
|
|
21
|
-
} else {
|
|
22
|
-
throw new Error('Import failed');
|
|
23
|
-
}
|
|
24
|
-
} catch (error) {
|
|
25
|
-
alert('Import failed: ' + error.message);
|
|
26
|
-
} finally {
|
|
27
|
-
event.target.value = '';
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return React.createElement('div', { style: { display: 'inline-block', marginLeft: '8px' } },
|
|
32
|
-
React.createElement('input', {
|
|
33
|
-
type: 'file',
|
|
34
|
-
accept: '.xlsx,.xls,.json',
|
|
35
|
-
onChange: handleImport,
|
|
36
|
-
style: { display: 'none' },
|
|
37
|
-
id: 'import-file-input'
|
|
38
|
-
}),
|
|
39
|
-
React.createElement('label', {
|
|
40
|
-
htmlFor: 'import-file-input',
|
|
41
|
-
style: {
|
|
42
|
-
display: 'inline-block',
|
|
43
|
-
padding: '8px 16px',
|
|
44
|
-
backgroundColor: '#328048',
|
|
45
|
-
color: 'white',
|
|
46
|
-
border: 'none',
|
|
47
|
-
borderRadius: '4px',
|
|
48
|
-
cursor: 'pointer'
|
|
49
|
-
}
|
|
50
|
-
}, 'Import Data')
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export default ImportButton;
|