@nkhang1902/strapi-plugin-export-import-clsx 1.0.3 → 1.0.4
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.
|
@@ -21,7 +21,7 @@ const ExportImportButtons = (props) => {
|
|
|
21
21
|
const filters = {};
|
|
22
22
|
|
|
23
23
|
for (const [key, value] of urlParams.entries()) {
|
|
24
|
-
if (key.startsWith('filters[') || key === 'sort' || key === 'page' || key === 'pageSize' || key === 'locale') {
|
|
24
|
+
if (key.startsWith('filters[') || key === 'sort' || key === 'page' || key === 'pageSize' || key === 'locale' || key === '_q') {
|
|
25
25
|
filters[key] = value;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -41,21 +41,54 @@ const ExportImportButtons = (props) => {
|
|
|
41
41
|
if (props.selection && props.selection.length > 0) {
|
|
42
42
|
return props.selection;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const selectedIds = [];
|
|
45
|
+
let field = '';
|
|
46
|
+
const getHeaderKey = i => {
|
|
47
|
+
const el = document.querySelector(`thead th:nth-child(${i}) button, thead th:nth-child(${i}) span`);
|
|
48
|
+
if (!el) return '';
|
|
49
|
+
const parts = el.textContent.trim().split(/\s+/);
|
|
50
|
+
return parts.pop(); // last word
|
|
51
|
+
};
|
|
52
|
+
|
|
46
53
|
try {
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
const rows = document.querySelectorAll('tbody tr');
|
|
55
|
+
const allowedFields = [
|
|
56
|
+
'id', 'name', 'title', 'tickerCode',
|
|
57
|
+
'fullName', 'email', 'businessEmail',
|
|
58
|
+
'telephone', 'mobile'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
let foundIndex = null;
|
|
62
|
+
|
|
63
|
+
for (let i = 1; i <= 10; i++) {
|
|
64
|
+
const headerBtn = getHeaderKey(i);
|
|
65
|
+
if (headerBtn !== '' && allowedFields.includes(headerBtn)) {
|
|
66
|
+
field = headerBtn;
|
|
67
|
+
foundIndex = i;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!foundIndex) {
|
|
73
|
+
console.warn('No valid header column found');
|
|
74
|
+
return [[], ''];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// gather values for selected rows
|
|
78
|
+
rows.forEach(row => {
|
|
79
|
+
const checkbox = row.querySelector('td:nth-child(1) button[role="checkbox"]');
|
|
80
|
+
if (checkbox?.getAttribute('aria-checked') === 'true') {
|
|
81
|
+
const cellSpan = row.querySelector(`td:nth-child(${foundIndex}) span`);
|
|
82
|
+
const text = cellSpan?.textContent.trim();
|
|
83
|
+
if (text) selectedIds.push(text);
|
|
54
84
|
}
|
|
55
85
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
|
|
87
|
+
return [selectedIds, field];
|
|
88
|
+
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error(e);
|
|
91
|
+
return [[], ''];
|
|
59
92
|
}
|
|
60
93
|
};
|
|
61
94
|
|
|
@@ -69,8 +102,8 @@ const ExportImportButtons = (props) => {
|
|
|
69
102
|
setIsExporting(true);
|
|
70
103
|
try {
|
|
71
104
|
const filters = getCurrentFilters();
|
|
72
|
-
const selectedEntries = getSelectedEntries();
|
|
73
|
-
|
|
105
|
+
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
106
|
+
|
|
74
107
|
const queryParams = new URLSearchParams({
|
|
75
108
|
format: 'excel',
|
|
76
109
|
contentType: contentType,
|
|
@@ -80,10 +113,11 @@ const ExportImportButtons = (props) => {
|
|
|
80
113
|
// Add selected IDs if any
|
|
81
114
|
if (selectedEntries.length > 0) {
|
|
82
115
|
queryParams.set('selectedIds', JSON.stringify(selectedEntries));
|
|
116
|
+
queryParams.set('selectedField', selectedField);
|
|
83
117
|
}
|
|
84
118
|
|
|
85
119
|
const response = await fetch(`/export-import-clsx/export?${queryParams}`);
|
|
86
|
-
|
|
120
|
+
|
|
87
121
|
if (response.ok) {
|
|
88
122
|
const blob = await response.blob();
|
|
89
123
|
const url = window.URL.createObjectURL(blob);
|
|
@@ -180,7 +214,7 @@ const ExportImportButtons = (props) => {
|
|
|
180
214
|
}
|
|
181
215
|
};
|
|
182
216
|
|
|
183
|
-
const selectedEntries = getSelectedEntries();
|
|
217
|
+
const [selectedEntries, selectedField] = getSelectedEntries();
|
|
184
218
|
const exportButtonText = isExporting
|
|
185
219
|
? 'Exporting...'
|
|
186
220
|
: selectedEntries.length > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkhang1902/strapi-plugin-export-import-clsx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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": {
|
|
@@ -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`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const XLSX = require('xlsx');
|
|
2
2
|
|
|
3
3
|
module.exports = ({ strapi }) => ({
|
|
4
|
-
async exportData(format = 'json', contentType = null,
|
|
4
|
+
async exportData(format = 'json', contentType = null, rawFilters = {}, selectedIds = [], selectedField = null) {
|
|
5
5
|
// Get only API content types (collections)
|
|
6
6
|
let contentTypes;
|
|
7
7
|
if (contentType) {
|
|
@@ -21,97 +21,77 @@ module.exports = ({ strapi }) => ({
|
|
|
21
21
|
for (const ct of contentTypes) {
|
|
22
22
|
try {
|
|
23
23
|
// Parse filters from URL format
|
|
24
|
-
const parsedFilters = this.parseFilters(
|
|
24
|
+
const parsedFilters = this.parseFilters(rawFilters);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
if (rawFilters['_q']) {
|
|
27
|
+
parsedFilters._q = rawFilters['_q'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
strapi.log.info(`Exporting ${ct} with raw filters: ${JSON.stringify(rawFilters)}`);
|
|
31
|
+
strapi.log.info(`Parsed filters: ${JSON.stringify(parsedFilters)}`);
|
|
32
|
+
strapi.log.info(`Selected IDs: ${JSON.stringify(selectedIds)}`);
|
|
29
33
|
|
|
30
34
|
let entries = [];
|
|
35
|
+
let filters = parsedFilters.filters;
|
|
31
36
|
|
|
32
37
|
// If specific IDs are selected, export only those
|
|
33
38
|
if (selectedIds && selectedIds.length > 0) {
|
|
39
|
+
strapi.log.info(`Exporting selected: ${JSON.stringify(selectedIds)}, field: ${selectedField}`);
|
|
40
|
+
if (selectedField === 'id' || (strapi.contentTypes[ct].attributes[selectedField] &&["number", "integer", "biginteger", "float", "decimal"].includes(strapi.contentTypes[ct].attributes[selectedField].type))) {
|
|
41
|
+
selectedIds = selectedIds.map((id) => Number(id));
|
|
42
|
+
}
|
|
34
43
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
populate: '*',
|
|
42
|
-
});
|
|
43
|
-
if (entry) {
|
|
44
|
-
entries.push(entry);
|
|
45
|
-
}
|
|
46
|
-
} catch (error) {
|
|
47
|
-
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
// Fallback for older Strapi versions
|
|
52
|
-
for (const id of selectedIds) {
|
|
53
|
-
try {
|
|
54
|
-
const entry = await strapi.entityService.findOne(ct, id, {
|
|
55
|
-
populate: '*',
|
|
56
|
-
});
|
|
57
|
-
if (entry) {
|
|
58
|
-
entries.push(entry);
|
|
59
|
-
}
|
|
60
|
-
} catch (error) {
|
|
61
|
-
strapi.log.warn(`Failed to find entry ${id}:`, error.message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
44
|
+
entries = await strapi.documents(ct).findMany({
|
|
45
|
+
filters: {
|
|
46
|
+
[selectedField]: { $in: selectedIds }
|
|
47
|
+
},
|
|
48
|
+
populate: '*'
|
|
49
|
+
});
|
|
65
50
|
} catch (error) {
|
|
66
51
|
strapi.log.error(`Failed to export selected entries:`, error);
|
|
67
52
|
}
|
|
68
53
|
} else {
|
|
69
54
|
// Export all entries with filters
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
uniqueEntries.set(docId, entry);
|
|
85
|
-
} else {
|
|
86
|
-
const existing = uniqueEntries.get(docId);
|
|
87
|
-
const existingIsPublished = !!existing.publishedAt;
|
|
88
|
-
const existingIsModified = existing.updatedAt !== existing.createdAt;
|
|
89
|
-
// Priority: published > modified draft > draft
|
|
90
|
-
if (isPublished && !existingIsPublished) {
|
|
91
|
-
uniqueEntries.set(docId, entry);
|
|
92
|
-
} else if (!isPublished && !existingIsPublished && isModified && !existingIsModified) {
|
|
93
|
-
uniqueEntries.set(docId, entry);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
55
|
+
const searchable = this.getSearchableFields(strapi.contentTypes[ct]);
|
|
56
|
+
const numberSearchable = this.getNumberFields(strapi.contentTypes[ct]);
|
|
57
|
+
|
|
58
|
+
if (parsedFilters._q) {
|
|
59
|
+
strapi.log.info(`Applying search query: ${parsedFilters._q} for fields: ${JSON.stringify([...searchable, ...numberSearchable])}`);
|
|
60
|
+
const orConditions = [];
|
|
61
|
+
|
|
62
|
+
if (searchable.length > 0) {
|
|
63
|
+
orConditions.push(
|
|
64
|
+
...searchable.map((field) => ({
|
|
65
|
+
[field]: { $containsi: parsedFilters._q }
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
97
69
|
|
|
98
|
-
|
|
99
|
-
|
|
70
|
+
if (numberSearchable.length > 0 && !isNaN(parsedFilters._q)) {
|
|
71
|
+
orConditions.push(
|
|
72
|
+
...numberSearchable.map((field) => ({
|
|
73
|
+
[field]: { $eq: Number(parsedFilters._q) }
|
|
74
|
+
}))
|
|
75
|
+
);
|
|
76
|
+
}
|
|
100
77
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
entries = await strapi.entityService.findMany(ct, {
|
|
110
|
-
populate: '*',
|
|
111
|
-
filters: parsedFilters,
|
|
112
|
-
});
|
|
113
|
-
strapi.log.info(`EntityService found ${entries?.length || 0} entries`);
|
|
78
|
+
if (orConditions.length > 0) {
|
|
79
|
+
filters = {
|
|
80
|
+
...filters,
|
|
81
|
+
$and: [
|
|
82
|
+
...(filters?.$and || []),
|
|
83
|
+
{ $or: orConditions }
|
|
84
|
+
]
|
|
85
|
+
};
|
|
114
86
|
}
|
|
87
|
+
}
|
|
88
|
+
strapi.log.info(`Parsed query filters: ${JSON.stringify(filters)}`)
|
|
89
|
+
try {
|
|
90
|
+
entries = await strapi.documents(ct).findMany({
|
|
91
|
+
filters: { ...filters},
|
|
92
|
+
populate: '*',
|
|
93
|
+
});
|
|
94
|
+
strapi.log.info(`EntityService found ${entries?.length || 0} entries`);
|
|
115
95
|
} catch (error) {
|
|
116
96
|
strapi.log.error(`Failed to query entries:`, error);
|
|
117
97
|
}
|
|
@@ -134,11 +114,37 @@ module.exports = ({ strapi }) => ({
|
|
|
134
114
|
return exportData;
|
|
135
115
|
},
|
|
136
116
|
|
|
117
|
+
getSearchableFields(contentTypeSchema) {
|
|
118
|
+
const searchable = [];
|
|
119
|
+
|
|
120
|
+
for (const [fieldName, field] of Object.entries(contentTypeSchema.attributes)) {
|
|
121
|
+
if (["string", "text", "richtext", "email", "uid", "enumeration"].includes(field.type) && fieldName !== 'locale') {
|
|
122
|
+
searchable.push(fieldName);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return searchable;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
getNumberFields(contentTypeSchema) {
|
|
130
|
+
const numberFields = [];
|
|
131
|
+
|
|
132
|
+
for (const [fieldName, field] of Object.entries(contentTypeSchema.attributes)) {
|
|
133
|
+
if (["number", "integer", "biginteger", "float", "decimal"].includes(field.type)) {
|
|
134
|
+
numberFields.push(fieldName);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
numberFields.push('id');
|
|
139
|
+
|
|
140
|
+
return numberFields;
|
|
141
|
+
},
|
|
142
|
+
|
|
137
143
|
parseFilters(filters) {
|
|
138
144
|
const parsed = {};
|
|
139
145
|
for (const [key, value] of Object.entries(filters)) {
|
|
140
146
|
// Skip pagination and sorting params
|
|
141
|
-
if (['page', 'pageSize', 'sort', 'locale', 'format', 'contentType', '
|
|
147
|
+
if (['page', 'pageSize', 'sort', 'locale', 'format', 'contentType', '_q'].includes(key)) {
|
|
142
148
|
continue;
|
|
143
149
|
}
|
|
144
150
|
|
|
@@ -172,62 +178,6 @@ module.exports = ({ strapi }) => ({
|
|
|
172
178
|
return parsed;
|
|
173
179
|
},
|
|
174
180
|
|
|
175
|
-
applyClientSideFilters(entries, filters) {
|
|
176
|
-
if (!filters || Object.keys(filters).length === 0) {
|
|
177
|
-
return entries;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const filtered = entries.filter(entry => {
|
|
181
|
-
// Handle structured filters
|
|
182
|
-
if (filters.filters && filters.filters.$and) {
|
|
183
|
-
for (const condition of filters.filters.$and) {
|
|
184
|
-
for (const [field, criteria] of Object.entries(condition)) {
|
|
185
|
-
if (typeof criteria === 'object' && criteria.$contains) {
|
|
186
|
-
// Handle $contains filter
|
|
187
|
-
if (entry[field]) {
|
|
188
|
-
const fieldValue = String(entry[field]).toLowerCase();
|
|
189
|
-
const searchValue = String(criteria.$contains).toLowerCase();
|
|
190
|
-
if (!fieldValue.includes(searchValue)) {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
return false; // Field doesn't exist, exclude entry
|
|
195
|
-
}
|
|
196
|
-
} else {
|
|
197
|
-
// Handle exact match
|
|
198
|
-
if (entry[field] !== criteria) {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Handle other filter formats
|
|
206
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
207
|
-
if (key === 'filters') continue; // Already handled above
|
|
208
|
-
|
|
209
|
-
// Handle simple search (global search)
|
|
210
|
-
if (key === '_q' || key === 'search') {
|
|
211
|
-
// Global search across main fields
|
|
212
|
-
const searchFields = ['shortName', 'name', 'title'];
|
|
213
|
-
const searchValue = String(value).toLowerCase();
|
|
214
|
-
const found = searchFields.some(field => {
|
|
215
|
-
if (entry[field]) {
|
|
216
|
-
return String(entry[field]).toLowerCase().includes(searchValue);
|
|
217
|
-
}
|
|
218
|
-
return false;
|
|
219
|
-
});
|
|
220
|
-
if (!found) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
return true;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
return filtered;
|
|
229
|
-
},
|
|
230
|
-
|
|
231
181
|
convertToExcel(data) {
|
|
232
182
|
const workbook = XLSX.utils.book_new();
|
|
233
183
|
let hasData = false;
|