@tunghtml/strapi-plugin-export-import-clsx 1.0.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/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @tunghtml/export-import-clsx
2
+
3
+ A powerful Strapi plugin for exporting and importing data with enhanced functionality, including Excel support and advanced filtering.
4
+
5
+ ## Features
6
+
7
+ - 📊 **Excel Export/Import**: Full support for .xlsx files
8
+ - 🔍 **Advanced Filtering**: Export filtered data based on UI filters
9
+ - 🎯 **Selective Export**: Export specific entries by selection
10
+ - 🌐 **Multi-locale Support**: Handle localized content properly
11
+ - 🔄 **Bulk Operations**: Import multiple entries efficiently
12
+ - 📝 **Smart Deduplication**: Avoid duplicate entries during import
13
+ - 🎨 **Clean UI**: Integrated seamlessly with Strapi admin panel
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @tunghtml/export-import-clsx
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ 1. Install the plugin in your Strapi project
24
+ 2. Add it to your `config/plugins.js`:
25
+
26
+ ```javascript
27
+ module.exports = {
28
+ 'export-import-clsx': {
29
+ enabled: true,
30
+ },
31
+ };
32
+ ```
33
+
34
+ 3. Restart your Strapi application
35
+ 4. Navigate to the plugin in your admin panel
36
+
37
+ ## API Endpoints
38
+
39
+ ### Export Data
40
+ ```
41
+ GET /export-import-clsx/export
42
+ ```
43
+
44
+ Query parameters:
45
+ - `format`: `excel` or `json` (default: `excel`)
46
+ - `contentType`: Specific content type to export (e.g., `api::article.article`)
47
+ - `selectedIds`: Array of specific entry IDs to export
48
+ - `filters[...]`: Advanced filtering options
49
+
50
+ ### Import Data
51
+ ```
52
+ POST /export-import-clsx/import
53
+ ```
54
+
55
+ Body: Excel file or JSON data
56
+
57
+ ## Examples
58
+
59
+ ### Export all articles as Excel
60
+ ```bash
61
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article"
62
+ ```
63
+
64
+ ### Export filtered data
65
+ ```bash
66
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article&filters[$and][0][title][$contains]=news"
67
+ ```
68
+
69
+ ### Export selected entries
70
+ ```bash
71
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article&selectedIds=[\"1\",\"2\",\"3\"]"
72
+ ```
73
+
74
+ ## Configuration
75
+
76
+ The plugin works out of the box with default settings. For advanced configuration, you can customize the behavior in your Strapi application.
77
+
78
+ ## Compatibility
79
+
80
+ - Strapi v4.x
81
+ - Strapi v5.x (with document service support)
82
+
83
+ ## Contributing
84
+
85
+ Contributions are welcome! Please feel free to submit a Pull Request.
86
+
87
+ ## License
88
+
89
+ MIT © tunghtml
90
+
91
+ ## Support
92
+
93
+ For issues and questions, please create an issue on the GitHub repository.
@@ -0,0 +1,70 @@
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;
@@ -0,0 +1,48 @@
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;
@@ -0,0 +1,245 @@
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;
@@ -0,0 +1,54 @@
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;
@@ -0,0 +1,15 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import pluginId from '../../pluginId';
4
+
5
+ const Initializer = ({ setPlugin }) => {
6
+ const ref = useRef(setPlugin);
7
+
8
+ useEffect(() => {
9
+ ref.current(pluginId);
10
+ }, []);
11
+
12
+ return null;
13
+ };
14
+
15
+ export default Initializer;
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { Download } from '@strapi/icons';
3
+
4
+ const PluginIcon = () => React.createElement(Download);
5
+
6
+ export default PluginIcon;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import HomePage from '../HomePage';
3
+
4
+ const App = () => {
5
+ return React.createElement(HomePage);
6
+ };
7
+
8
+ export default App;