@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
- // Try to get from global state or context
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
- // 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
+ 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
- return selectedIds;
57
- } catch (error) {
58
- return [];
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",
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, filters = {}, selectedIds = []) {
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(filters);
24
+ const parsedFilters = this.parseFilters(rawFilters);
25
25
 
26
- strapi.log.info(`Exporting ${ct} with raw filters:`, filters);
27
- strapi.log.info(`Parsed filters:`, parsedFilters);
28
- strapi.log.info(`Selected IDs:`, selectedIds);
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
- if (strapi.documents) {
36
- // Get entries by documentId for Strapi v5
37
- for (const id of selectedIds) {
38
- try {
39
- const entry = await strapi.documents(ct).findOne({
40
- documentId: id,
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
- try {
71
- if (strapi.documents) {
72
- // Get all entries (both published and draft) but avoid duplicates
73
- const allEntries = await strapi.documents(ct).findMany({
74
- populate: '*',
75
- // Don't specify status to get all
76
- });
77
- // Group by documentId and keep only the best version (published > modified draft > draft)
78
- const uniqueEntries = new Map();
79
- for (const entry of allEntries) {
80
- const docId = entry.documentId;
81
- const isPublished = !!entry.publishedAt;
82
- const isModified = entry.updatedAt !== entry.createdAt;
83
- if (!uniqueEntries.has(docId)) {
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
- entries = Array.from(uniqueEntries.values());
99
- strapi.log.info(`Found ${allEntries.length} total entries, ${entries.length} unique entries after deduplication`);
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
- // Apply filters
102
- if (parsedFilters && Object.keys(parsedFilters).length > 0) {
103
- strapi.log.info('Applying filters:', parsedFilters);
104
- entries = this.applyClientSideFilters(entries, parsedFilters);
105
- strapi.log.info(`After filtering: ${entries.length} entries`);
106
- }
107
- } else {
108
- // Fallback for older Strapi versions
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', 'selectedIds'].includes(key)) {
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;