@nkhang1902/strapi-plugin-export-import-clsx 1.0.3

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,95 @@
1
+ # @tunghtml/strapi-plugin-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/strapi-plugin-export-import-clsx
19
+ # or
20
+ yarn add @tunghtml/strapi-plugin-export-import-clsx
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ 1. Install the plugin in your Strapi project
26
+ 2. Add it to your `config/plugins.js`:
27
+
28
+ ```javascript
29
+ module.exports = {
30
+ 'export-import-clsx': {
31
+ enabled: true,
32
+ },
33
+ };
34
+ ```
35
+
36
+ 3. Restart your Strapi application
37
+ 4. Navigate to the plugin in your admin panel
38
+
39
+ ## API Endpoints
40
+
41
+ ### Export Data
42
+ ```
43
+ GET /export-import-clsx/export
44
+ ```
45
+
46
+ Query parameters:
47
+ - `format`: `excel` or `json` (default: `excel`)
48
+ - `contentType`: Specific content type to export (e.g., `api::article.article`)
49
+ - `selectedIds`: Array of specific entry IDs to export
50
+ - `filters[...]`: Advanced filtering options
51
+
52
+ ### Import Data
53
+ ```
54
+ POST /export-import-clsx/import
55
+ ```
56
+
57
+ Body: Excel file or JSON data
58
+
59
+ ## Examples
60
+
61
+ ### Export all articles as Excel
62
+ ```bash
63
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article"
64
+ ```
65
+
66
+ ### Export filtered data
67
+ ```bash
68
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article&filters[$and][0][title][$contains]=news"
69
+ ```
70
+
71
+ ### Export selected entries
72
+ ```bash
73
+ curl "http://localhost:1337/export-import-clsx/export?format=excel&contentType=api::article.article&selectedIds=[\"1\",\"2\",\"3\"]"
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ The plugin works out of the box with default settings. For advanced configuration, you can customize the behavior in your Strapi application.
79
+
80
+ ## Compatibility
81
+
82
+ - Strapi v4.x
83
+ - Strapi v5.x (with document service support)
84
+
85
+ ## Contributing
86
+
87
+ Contributions are welcome! Please feel free to submit a Pull Request.
88
+
89
+ ## License
90
+
91
+ MIT © finnwasabi
92
+
93
+ ## Support
94
+
95
+ 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;