@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 +93 -0
- package/admin/src/components/BulkActions/index.js +70 -0
- package/admin/src/components/ExportButton/index.js +48 -0
- package/admin/src/components/ExportImportButtons/index.js +245 -0
- package/admin/src/components/ImportButton/index.js +54 -0
- package/admin/src/components/Initializer/index.js +15 -0
- package/admin/src/components/PluginIcon/index.js +6 -0
- package/admin/src/pages/App/index.js +8 -0
- package/admin/src/pages/HomePage/index.js +297 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/en.json +14 -0
- package/package.json +60 -0
- package/server/bootstrap.js +3 -0
- package/server/config/index.js +4 -0
- package/server/content-types/index.js +1 -0
- package/server/controllers/export-controller.js +62 -0
- package/server/controllers/import-controller.js +42 -0
- package/server/controllers/index.js +7 -0
- package/server/destroy.js +3 -0
- package/server/middlewares/index.js +1 -0
- package/server/policies/index.js +1 -0
- package/server/register.js +3 -0
- package/server/routes/index.js +29 -0
- package/server/services/export-service.js +336 -0
- package/server/services/import-service.js +306 -0
- package/server/services/index.js +7 -0
- package/strapi-admin.js +88 -0
- package/strapi-server.js +34 -0
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;
|