@visns-studio/visns-components 5.10.11 → 5.11.1

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 CHANGED
@@ -93,6 +93,91 @@ const App = () => {
93
93
  export default App;
94
94
  ```
95
95
 
96
+ ## New Modular Architecture (v5.10.11+)
97
+
98
+ ### GenericIndex Component Refactoring
99
+
100
+ Starting from version 5.10.11, the GenericIndex component has been refactored for better maintainability and reusability:
101
+
102
+ **Before**: Single monolithic component (2,281 lines)
103
+ **After**: Modular architecture with separated concerns
104
+
105
+ #### New Components
106
+
107
+ The refactoring introduced two new standalone components that can be used independently:
108
+
109
+ **GenericGrid** - Standalone grid component with full data management capabilities:
110
+
111
+ ```jsx
112
+ import { GenericGrid } from 'visns-components';
113
+
114
+ <GenericGrid
115
+ config={{
116
+ settings: {
117
+ fetch: {
118
+ url: '/api/data',
119
+ method: 'POST',
120
+ params: { filter: 'active' }
121
+ }
122
+ },
123
+ columns: [
124
+ { name: 'id', header: 'ID', type: 'number' },
125
+ { name: 'name', header: 'Name', type: 'text' },
126
+ { name: 'date', header: 'Date', type: 'date' }
127
+ ]
128
+ }}
129
+ userProfile={userProfile}
130
+ onDataChange={(data) => console.log('Data updated:', data)}
131
+ onRowClick={(row) => console.log('Row clicked:', row)}
132
+ />
133
+ ```
134
+
135
+ **GenericReportForm** - Standalone export form component:
136
+
137
+ ```jsx
138
+ import { GenericReportForm } from 'visns-components';
139
+
140
+ <GenericReportForm
141
+ config={{
142
+ form: {
143
+ title: 'Export Data',
144
+ description: 'Select your export criteria below',
145
+ filters: [
146
+ { id: 'startDate', type: 'date', label: 'Start Date', required: true },
147
+ { id: 'endDate', type: 'date', label: 'End Date', required: true },
148
+ { id: 'format', type: 'dropdown', label: 'Format', options: [
149
+ { value: 'csv', label: 'CSV' },
150
+ { value: 'xlsx', label: 'Excel' }
151
+ ]}
152
+ ],
153
+ url: '/api/export',
154
+ method: 'POST',
155
+ filename: 'export.csv'
156
+ }
157
+ }}
158
+ userProfile={userProfile}
159
+ onExport={(formData) => customExportHandler(formData)}
160
+ onFormChange={(formData) => console.log('Form changed:', formData)}
161
+ />
162
+ ```
163
+
164
+ #### Benefits of the Refactoring
165
+
166
+ - **Reduced complexity**: GenericIndex reduced from 2,281 to 1,031 lines (55% reduction)
167
+ - **Enhanced reusability**: Grid and form components can be used independently
168
+ - **Better maintainability**: Focused components are easier to debug and modify
169
+ - **Improved testing**: Components can be unit tested separately
170
+ - **Backward compatibility**: All existing GenericIndex usage remains unchanged
171
+
172
+ #### Shared Utilities
173
+
174
+ The refactoring also introduced shared utility functions:
175
+
176
+ - **`shared/formatters.js`**: Cell content formatting and styling utilities
177
+ - **`shared/gridUtils.js`**: Data operations (sorting, filtering, transformations)
178
+
179
+ These utilities are available for use in custom components and provide consistent functionality across the library.
180
+
96
181
  ## Component Documentation
97
182
 
98
183
  ### Authentication Components
package/package.json CHANGED
@@ -24,17 +24,17 @@
24
24
  "dayjs": "^1.11.13",
25
25
  "fabric": "^6.7.0",
26
26
  "file-saver": "^2.0.5",
27
- "framer-motion": "^12.17.0",
27
+ "framer-motion": "^12.18.1",
28
28
  "html-react-parser": "^5.2.5",
29
29
  "lodash": "^4.17.21",
30
30
  "lodash.debounce": "^4.0.8",
31
31
  "moment": "^2.30.1",
32
- "motion": "^12.17.0",
32
+ "motion": "^12.18.1",
33
33
  "numeral": "^2.0.6",
34
34
  "pluralize": "^8.0.0",
35
35
  "qrcode.react": "^4.2.0",
36
36
  "quill-image-uploader": "^1.3.0",
37
- "react-big-calendar": "^1.19.2",
37
+ "react-big-calendar": "^1.19.3",
38
38
  "react-color": "^2.19.3",
39
39
  "react-copy-to-clipboard": "^5.1.0",
40
40
  "react-datepicker": "^8.4.0",
@@ -53,7 +53,7 @@
53
53
  "react-to-print": "^3.1.0",
54
54
  "react-toastify": "^11.0.5",
55
55
  "react-toggle": "^4.1.3",
56
- "react-tooltip": "^5.29.0",
56
+ "react-tooltip": "^5.29.1",
57
57
  "react-window": "^1.8.11",
58
58
  "reactjs-popup": "^2.0.6",
59
59
  "style-loader": "^4.0.0",
@@ -65,7 +65,7 @@
65
65
  "validator": "^13.15.15",
66
66
  "vite": "^6.3.5",
67
67
  "yarn": "^1.22.22",
68
- "yet-another-react-lightbox": "^3.23.2"
68
+ "yet-another-react-lightbox": "^3.23.3"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@babel/core": "^7.27.4",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.10.11",
90
+ "version": "5.11.1",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -0,0 +1,380 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import Swal from 'sweetalert2';
3
+ import './styles/SweetAlert.module.css';
4
+ import './styles/ContactSelectorModal.css';
5
+ import CustomFetch from '../Fetch';
6
+
7
+ /**
8
+ * A utility function that shows a contact selector modal using SweetAlert2
9
+ * Used after date selection to add/select contacts for a survey field
10
+ *
11
+ * @param {Object} options - Configuration options
12
+ * @param {string} options.title - The title of the dialog (default: 'Select Contacts')
13
+ * @param {string} options.message - The message to display in the dialog
14
+ * @param {string} options.confirmLabel - Label for the confirm button (default: 'Save')
15
+ * @param {string} options.cancelLabel - Label for the cancel button (default: 'Cancel')
16
+ * @param {Function} options.onConfirm - Function to call when confirmed with selected contacts
17
+ * @param {Function} options.onCancel - Function to call when cancelled
18
+ * @param {Array} options.existingContacts - Array of existing contacts for this field
19
+ * @param {Object} options.contactConfig - Contact selection configuration
20
+ * @param {Object} options.data - Optional data object to pass context
21
+ * @returns {Promise} - A promise that resolves when the dialog is closed
22
+ */
23
+ export const showContactSelectorModal = ({
24
+ title = 'Select Contacts',
25
+ message = 'Add contacts for this survey:',
26
+ confirmLabel = 'Save',
27
+ cancelLabel = 'Cancel',
28
+ onConfirm = () => {},
29
+ onCancel = () => {},
30
+ existingContacts = [],
31
+ contactConfig = {},
32
+ data = null,
33
+ rowContext = null,
34
+ }) => {
35
+ const {
36
+ contactsUrl = '/ajax/contacts/dropdown',
37
+ urlParams = {},
38
+ allowManualEntry = true,
39
+ fields = ['name', 'email'],
40
+ multiple = true,
41
+ maxContacts = 10
42
+ } = contactConfig;
43
+
44
+ // State management for the modal
45
+ let modalState = {
46
+ contacts: [...existingContacts],
47
+ availableContacts: [],
48
+ isAddingManual: false,
49
+ newContact: {}
50
+ };
51
+
52
+ // Initialize new contact object
53
+ fields.forEach(field => {
54
+ modalState.newContact[field] = '';
55
+ });
56
+
57
+ // Process URL parameters with row context substitution
58
+ const processUrlParams = (params, rowData) => {
59
+ if (!params || typeof params !== 'object') return {};
60
+
61
+ console.log('Processing URL params:', {
62
+ params,
63
+ rowData,
64
+ rowDataKeys: rowData ? Object.keys(rowData) : 'no rowData'
65
+ });
66
+
67
+ const processedParams = {};
68
+
69
+ for (const [key, value] of Object.entries(params)) {
70
+ if (Array.isArray(value)) {
71
+ // Handle arrays (like 'where' parameter)
72
+ processedParams[key] = value.map(item => {
73
+ if (typeof item === 'object' && item !== null) {
74
+ const processedItem = {};
75
+ for (const [itemKey, itemValue] of Object.entries(item)) {
76
+ if (typeof itemValue === 'string' && itemValue.startsWith('{row.') && itemValue.endsWith('}')) {
77
+ // Extract field name from {row.fieldName}
78
+ const fieldName = itemValue.slice(5, -1);
79
+ const actualValue = rowData?.[fieldName];
80
+ console.log(`Replacing ${itemValue} with field ${fieldName}:`, actualValue);
81
+ processedItem[itemKey] = actualValue !== undefined ? actualValue : itemValue;
82
+ } else {
83
+ processedItem[itemKey] = itemValue;
84
+ }
85
+ }
86
+ return processedItem;
87
+ }
88
+ return item;
89
+ });
90
+ } else if (typeof value === 'string' && value.startsWith('{row.') && value.endsWith('}')) {
91
+ // Handle simple string substitution
92
+ const fieldName = value.slice(5, -1);
93
+ const actualValue = rowData?.[fieldName];
94
+ console.log(`Replacing ${value} with field ${fieldName}:`, actualValue);
95
+ processedParams[key] = actualValue !== undefined ? actualValue : value;
96
+ } else {
97
+ processedParams[key] = value;
98
+ }
99
+ }
100
+
101
+ console.log('Processed params result:', processedParams);
102
+ return processedParams;
103
+ };
104
+
105
+ // Fetch available contacts from API
106
+ const fetchContacts = async () => {
107
+ try {
108
+ // Process URL parameters with row context
109
+ const requestParams = processUrlParams(urlParams, rowContext);
110
+
111
+ console.log('Fetching contacts with params:', {
112
+ url: contactsUrl,
113
+ originalParams: urlParams,
114
+ processedParams: requestParams,
115
+ rowContext: rowContext
116
+ });
117
+
118
+ const response = await CustomFetch(contactsUrl, 'POST', requestParams);
119
+ if (response.data && response.data.data) {
120
+ modalState.availableContacts = response.data.data.map(contact => ({
121
+ value: contact.id || contact.value,
122
+ label: contact.name || contact.label || contact.title,
123
+ ...contact
124
+ }));
125
+
126
+ console.log(`Fetched ${modalState.availableContacts.length} contacts`);
127
+ }
128
+ } catch (error) {
129
+ console.warn('Failed to fetch contacts:', error);
130
+ modalState.availableContacts = [];
131
+ }
132
+ };
133
+
134
+ // Generate HTML for the modal
135
+ const generateModalHTML = () => {
136
+ const contactsHtml = modalState.contacts.map((contact, index) => `
137
+ <div class="contact-item" data-index="${index}">
138
+ <div class="contact-info">
139
+ <strong>${contact.name || 'Unknown Contact'}</strong>
140
+ ${contact.email ? `<br><small>${contact.email}</small>` : ''}
141
+ <span class="contact-type">${contact.type === 'contact' ? 'Existing Contact' : 'Manual Entry'}</span>
142
+ </div>
143
+ <button type="button" class="remove-contact-btn" data-index="${index}">×</button>
144
+ </div>
145
+ `).join('');
146
+
147
+ const availableContactsOptions = modalState.availableContacts.map(contact =>
148
+ `<option value="${contact.value}">${contact.label}</option>`
149
+ ).join('');
150
+
151
+ const manualFieldsHtml = fields.map(field => `
152
+ <div class="form-group">
153
+ <div class="form-field-row">
154
+ <label for="manual-${field}">${field.charAt(0).toUpperCase() + field.slice(1)}:</label>
155
+ <input
156
+ type="${field === 'email' ? 'email' : 'text'}"
157
+ id="manual-${field}"
158
+ class="swal2-input"
159
+ placeholder="Enter ${field}"
160
+ ${field === 'email' ? 'required' : ''}
161
+ >
162
+ </div>
163
+ </div>
164
+ `).join('');
165
+
166
+ return `
167
+ <div class="contact-selector-modal">
168
+ <div class="contact-modal-header">
169
+ <p class="contact-modal-message">${message}</p>
170
+ ${data ? `<small class="field-info">Field: ${data.label || data.name}</small>` : ''}
171
+ </div>
172
+
173
+ <div class="existing-contacts">
174
+ <h4>Selected Contacts (${modalState.contacts.length})</h4>
175
+ <div class="contacts-list">
176
+ ${contactsHtml || '<p class="no-contacts">No contacts selected</p>'}
177
+ </div>
178
+ </div>
179
+
180
+ <div class="add-contact-section">
181
+ <h4>Add Contact</h4>
182
+
183
+ ${modalState.availableContacts.length > 0 ? `
184
+ <div class="existing-contact-selector">
185
+ <label for="existing-contact-select">Select Existing Contact:</label>
186
+ <div class="contact-select-row">
187
+ <select id="existing-contact-select" class="swal2-select">
188
+ <option value="">Choose a contact...</option>
189
+ ${availableContactsOptions}
190
+ </select>
191
+ <button type="button" id="add-existing-contact" class="add-contact-btn">Add</button>
192
+ </div>
193
+ </div>
194
+ ` : ''}
195
+
196
+ ${allowManualEntry ? `
197
+ <div class="manual-contact-section">
198
+ <div class="manual-contact-toggle">
199
+ <button type="button" id="toggle-manual-entry" class="toggle-manual-btn">
200
+ ${modalState.isAddingManual ? 'Cancel Manual Entry' : 'Add Manual Contact'}
201
+ </button>
202
+ </div>
203
+
204
+ <div id="manual-contact-form" class="manual-contact-form" style="display: ${modalState.isAddingManual ? 'block' : 'none'}">
205
+ ${manualFieldsHtml}
206
+ <button type="button" id="add-manual-contact" class="add-contact-btn">Add Manual Contact</button>
207
+ </div>
208
+ </div>
209
+ ` : ''}
210
+ </div>
211
+
212
+ ${multiple && maxContacts > 1 ? `
213
+ <div class="contact-limits">
214
+ <small>Maximum ${maxContacts} contacts allowed</small>
215
+ </div>
216
+ ` : ''}
217
+ </div>
218
+ `;
219
+ };
220
+
221
+ // Event handlers
222
+ const setupEventHandlers = () => {
223
+ // Remove contact handler
224
+ document.addEventListener('click', (e) => {
225
+ if (e.target.classList.contains('remove-contact-btn')) {
226
+ const index = parseInt(e.target.dataset.index);
227
+ modalState.contacts.splice(index, 1);
228
+ updateModalContent();
229
+ }
230
+ });
231
+
232
+ // Add existing contact handler
233
+ const addExistingBtn = document.getElementById('add-existing-contact');
234
+ if (addExistingBtn) {
235
+ addExistingBtn.addEventListener('click', () => {
236
+ const select = document.getElementById('existing-contact-select');
237
+ const selectedValue = select.value;
238
+
239
+ if (selectedValue) {
240
+ const selectedContact = modalState.availableContacts.find(c => c.value == selectedValue);
241
+ if (selectedContact && !modalState.contacts.find(c => c.contact_id == selectedValue)) {
242
+ modalState.contacts.push({
243
+ type: 'contact',
244
+ contact_id: selectedContact.value,
245
+ name: selectedContact.label,
246
+ email: selectedContact.email || ''
247
+ });
248
+ select.value = '';
249
+ updateModalContent();
250
+ }
251
+ }
252
+ });
253
+ }
254
+
255
+ // Toggle manual entry
256
+ const toggleBtn = document.getElementById('toggle-manual-entry');
257
+ if (toggleBtn) {
258
+ toggleBtn.addEventListener('click', () => {
259
+ modalState.isAddingManual = !modalState.isAddingManual;
260
+ updateModalContent();
261
+ });
262
+ }
263
+
264
+ // Add manual contact handler
265
+ const addManualBtn = document.getElementById('add-manual-contact');
266
+ if (addManualBtn) {
267
+ addManualBtn.addEventListener('click', () => {
268
+ const newContact = { type: 'manual' };
269
+ let isValid = true;
270
+
271
+ fields.forEach(field => {
272
+ const input = document.getElementById(`manual-${field}`);
273
+ const value = input ? input.value.trim() : '';
274
+
275
+ if (field === 'email' && value && !isValidEmail(value)) {
276
+ isValid = false;
277
+ input.style.borderColor = '#dc2626';
278
+ return;
279
+ }
280
+
281
+ if (field === 'name' && !value) {
282
+ isValid = false;
283
+ input.style.borderColor = '#dc2626';
284
+ return;
285
+ }
286
+
287
+ newContact[field] = value;
288
+ if (input) input.style.borderColor = '';
289
+ });
290
+
291
+ if (isValid && newContact.name) {
292
+ modalState.contacts.push(newContact);
293
+
294
+ // Clear form
295
+ fields.forEach(field => {
296
+ const input = document.getElementById(`manual-${field}`);
297
+ if (input) input.value = '';
298
+ });
299
+
300
+ modalState.isAddingManual = false;
301
+ updateModalContent();
302
+ }
303
+ });
304
+ }
305
+ };
306
+
307
+ // Update modal content
308
+ const updateModalContent = () => {
309
+ const container = document.querySelector('.contact-selector-modal');
310
+ if (container) {
311
+ container.innerHTML = generateModalHTML().match(/<div class="contact-selector-modal">([\s\S]*)<\/div>/)[1];
312
+ setupEventHandlers();
313
+ }
314
+ };
315
+
316
+ // Email validation helper
317
+ const isValidEmail = (email) => {
318
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
319
+ };
320
+
321
+ // Validation function
322
+ const validateContacts = () => {
323
+ if (!multiple && modalState.contacts.length > 1) {
324
+ Swal.showValidationMessage('Only one contact is allowed for this field');
325
+ return false;
326
+ }
327
+
328
+ if (modalState.contacts.length > maxContacts) {
329
+ Swal.showValidationMessage(`Maximum ${maxContacts} contacts allowed`);
330
+ return false;
331
+ }
332
+
333
+ return true;
334
+ };
335
+
336
+ // Initialize and show modal
337
+ const showModal = async () => {
338
+ await fetchContacts();
339
+
340
+ const swalOptions = {
341
+ title: title,
342
+ html: generateModalHTML(),
343
+ showCancelButton: true,
344
+ confirmButtonText: confirmLabel,
345
+ cancelButtonText: cancelLabel,
346
+ confirmButtonColor: 'var(--primary-color, #4f46e5)',
347
+ cancelButtonColor: '#6b7280',
348
+ allowOutsideClick: false,
349
+ allowEscapeKey: true,
350
+ width: '600px',
351
+ padding: '1.25rem',
352
+ customClass: {
353
+ popup: 'contact-selector-dialog',
354
+ htmlContainer: 'contact-selector-content'
355
+ },
356
+ preConfirm: () => {
357
+ if (!validateContacts()) {
358
+ return false;
359
+ }
360
+ return modalState.contacts;
361
+ },
362
+ didOpen: () => {
363
+ setupEventHandlers();
364
+ }
365
+ };
366
+
367
+ return Swal.fire(swalOptions).then((result) => {
368
+ if (result.isConfirmed && result.value) {
369
+ console.log('Contacts selected:', result.value);
370
+ onConfirm(result.value);
371
+ } else if (result.dismiss === Swal.DismissReason.cancel) {
372
+ onCancel();
373
+ }
374
+ });
375
+ };
376
+
377
+ return showModal();
378
+ };
379
+
380
+ export default showContactSelectorModal;