@visns-studio/visns-components 5.10.10 → 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 +85 -0
- package/package.json +6 -6
- package/src/components/cms/DataGrid.jsx +83 -3
- package/src/components/crm/columns/ColumnRenderers.jsx +97 -2
- package/src/components/crm/generic/ContactSelectorModal.jsx +380 -0
- package/src/components/crm/generic/DatePickerDialog.jsx +302 -0
- package/src/components/crm/generic/GenericDetail.jsx +67 -2
- package/src/components/crm/generic/GenericGrid.jsx +1534 -0
- package/src/components/crm/generic/GenericIndex.jsx +5 -1255
- package/src/components/crm/generic/GenericReportForm.jsx +194 -0
- package/src/components/crm/generic/shared/formatters.js +125 -0
- package/src/components/crm/generic/shared/gridUtils.js +194 -0
- package/src/components/crm/generic/styles/ContactSelectorModal.css +367 -0
- package/src/components/crm/generic/styles/DatePickerDialog.css +84 -0
- package/src/components/crm/generic/styles/GenericDetail.module.scss +77 -0
- package/src/components/crm/generic/styles/GenericIndex.module.scss +176 -0
- package/src/index.js +4 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": [
|
|
@@ -763,9 +763,89 @@ const DataGrid = ({
|
|
|
763
763
|
data[column.id] &&
|
|
764
764
|
data[column.id][column.jsonData]
|
|
765
765
|
) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return
|
|
766
|
+
const jsonValue = data[column.id][column.jsonData];
|
|
767
|
+
|
|
768
|
+
if (!jsonValue) return null;
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const jsonData = typeof jsonValue === 'string' ? JSON.parse(jsonValue) : jsonValue;
|
|
772
|
+
|
|
773
|
+
// Check if this looks like a question/answer format
|
|
774
|
+
const isQuestionAnswerFormat = Object.values(jsonData).every(item =>
|
|
775
|
+
item && typeof item === 'object' &&
|
|
776
|
+
(item.hasOwnProperty('question') || item.hasOwnProperty('answer'))
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
if (isQuestionAnswerFormat) {
|
|
780
|
+
return (
|
|
781
|
+
<div style={{ fontSize: '0.8rem' }}>
|
|
782
|
+
{Object.entries(jsonData).map(([key, item]) => {
|
|
783
|
+
if (!item || typeof item !== 'object') return null;
|
|
784
|
+
|
|
785
|
+
const question = item.question || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
786
|
+
const answer = item.answer !== undefined ?
|
|
787
|
+
(typeof item.answer === 'boolean' ? (item.answer ? 'Yes' : 'No') : String(item.answer)) :
|
|
788
|
+
'N/A';
|
|
789
|
+
|
|
790
|
+
return (
|
|
791
|
+
<div key={key} style={{ marginBottom: '0.3rem' }}>
|
|
792
|
+
<div style={{ fontWeight: '600', color: 'var(--primary-color)', marginBottom: '0.1rem', fontSize: '0.75rem' }}>
|
|
793
|
+
{question.length > 60 ? question.substring(0, 60) + '...' : question}
|
|
794
|
+
</div>
|
|
795
|
+
<div style={{ marginLeft: '0.5rem', color: 'var(--paragraph-color)', fontSize: '0.75rem' }}>
|
|
796
|
+
{answer}
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
);
|
|
800
|
+
})}
|
|
801
|
+
</div>
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Check if this is a simple key-value object
|
|
806
|
+
const isSimpleKeyValue = Object.values(jsonData).every(val =>
|
|
807
|
+
typeof val !== 'object' || val === null
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
if (isSimpleKeyValue) {
|
|
811
|
+
return (
|
|
812
|
+
<div style={{ fontSize: '0.8rem' }}>
|
|
813
|
+
{Object.entries(jsonData).slice(0, 3).map(([key, val]) => (
|
|
814
|
+
<div key={key} style={{ marginBottom: '0.2rem', display: 'flex', gap: '0.3rem' }}>
|
|
815
|
+
<strong style={{ color: 'var(--primary-color)', fontSize: '0.7rem' }}>
|
|
816
|
+
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
|
|
817
|
+
</strong>
|
|
818
|
+
<span style={{ color: 'var(--paragraph-color)', fontSize: '0.7rem' }}>
|
|
819
|
+
{val !== null && val !== undefined ?
|
|
820
|
+
(typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val).substring(0, 20)) :
|
|
821
|
+
'N/A'}
|
|
822
|
+
</span>
|
|
823
|
+
</div>
|
|
824
|
+
))}
|
|
825
|
+
{Object.keys(jsonData).length > 3 && (
|
|
826
|
+
<div style={{ fontSize: '0.65rem', color: 'var(--secondary-color)', fontStyle: 'italic' }}>
|
|
827
|
+
+{Object.keys(jsonData).length - 3} more...
|
|
828
|
+
</div>
|
|
829
|
+
)}
|
|
830
|
+
</div>
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// For complex nested objects, show a summary
|
|
835
|
+
const keyCount = Object.keys(jsonData).length;
|
|
836
|
+
return (
|
|
837
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--paragraph-color)', fontStyle: 'italic' }}>
|
|
838
|
+
JSON ({keyCount} {keyCount === 1 ? 'property' : 'properties'})
|
|
839
|
+
</div>
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
} catch (error) {
|
|
843
|
+
// If parsing fails, show raw value truncated
|
|
844
|
+
const displayValue = typeof jsonValue === 'string' && jsonValue.length > 30
|
|
845
|
+
? jsonValue.substring(0, 30) + '...'
|
|
846
|
+
: jsonValue;
|
|
847
|
+
return <span style={{ fontSize: '0.75rem', fontFamily: 'monospace', color: 'var(--paragraph-color)' }}>{displayValue}</span>;
|
|
848
|
+
}
|
|
769
849
|
} else {
|
|
770
850
|
return null;
|
|
771
851
|
}
|
|
@@ -376,13 +376,108 @@ export const renderJsonColumn = ({ column, commonProps }) => {
|
|
|
376
376
|
data[column.id][column.jsonData]
|
|
377
377
|
) {
|
|
378
378
|
const jsonValue = data[column.id][column.jsonData];
|
|
379
|
+
|
|
380
|
+
if (!jsonValue) return null;
|
|
381
|
+
|
|
382
|
+
let displayContent;
|
|
383
|
+
let tooltipValue = jsonValue;
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const jsonData = typeof jsonValue === 'string' ? JSON.parse(jsonValue) : jsonValue;
|
|
387
|
+
|
|
388
|
+
// Check if this looks like a question/answer format
|
|
389
|
+
const isQuestionAnswerFormat = Object.values(jsonData).every(item =>
|
|
390
|
+
item && typeof item === 'object' &&
|
|
391
|
+
(item.hasOwnProperty('question') || item.hasOwnProperty('answer'))
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
if (isQuestionAnswerFormat) {
|
|
395
|
+
const entries = Object.entries(jsonData);
|
|
396
|
+
if (entries.length === 1) {
|
|
397
|
+
const [key, item] = entries[0];
|
|
398
|
+
const question = item.question || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
399
|
+
const answer = item.answer !== undefined ?
|
|
400
|
+
(typeof item.answer === 'boolean' ? (item.answer ? 'Yes' : 'No') : String(item.answer)) :
|
|
401
|
+
'N/A';
|
|
402
|
+
|
|
403
|
+
displayContent = (
|
|
404
|
+
<div style={{ fontSize: '0.8rem' }}>
|
|
405
|
+
<div style={{ fontWeight: '600', color: 'var(--primary-color)', marginBottom: '0.1rem', fontSize: '0.75rem' }}>
|
|
406
|
+
{question.length > 50 ? question.substring(0, 50) + '...' : question}
|
|
407
|
+
</div>
|
|
408
|
+
<div style={{ color: 'var(--paragraph-color)', fontSize: '0.75rem' }}>
|
|
409
|
+
{answer}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
} else {
|
|
414
|
+
displayContent = (
|
|
415
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--paragraph-color)' }}>
|
|
416
|
+
Q&A ({entries.length} items)
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
tooltipValue = JSON.stringify(jsonData, null, 2);
|
|
421
|
+
}
|
|
422
|
+
// Check if this is a simple key-value object
|
|
423
|
+
else if (Object.values(jsonData).every(val => typeof val !== 'object' || val === null)) {
|
|
424
|
+
const entries = Object.entries(jsonData);
|
|
425
|
+
if (entries.length <= 2) {
|
|
426
|
+
displayContent = (
|
|
427
|
+
<div style={{ fontSize: '0.8rem' }}>
|
|
428
|
+
{entries.map(([key, val]) => (
|
|
429
|
+
<div key={key} style={{ marginBottom: '0.1rem', display: 'flex', gap: '0.3rem' }}>
|
|
430
|
+
<strong style={{ color: 'var(--primary-color)', fontSize: '0.7rem' }}>
|
|
431
|
+
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
|
|
432
|
+
</strong>
|
|
433
|
+
<span style={{ color: 'var(--paragraph-color)', fontSize: '0.7rem' }}>
|
|
434
|
+
{val !== null && val !== undefined ?
|
|
435
|
+
(typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val).substring(0, 15)) :
|
|
436
|
+
'N/A'}
|
|
437
|
+
</span>
|
|
438
|
+
</div>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
displayContent = (
|
|
444
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--paragraph-color)' }}>
|
|
445
|
+
Data ({entries.length} properties)
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
tooltipValue = JSON.stringify(jsonData, null, 2);
|
|
450
|
+
}
|
|
451
|
+
// For complex nested objects
|
|
452
|
+
else {
|
|
453
|
+
const keyCount = Object.keys(jsonData).length;
|
|
454
|
+
displayContent = (
|
|
455
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--paragraph-color)', fontStyle: 'italic' }}>
|
|
456
|
+
JSON ({keyCount} {keyCount === 1 ? 'property' : 'properties'})
|
|
457
|
+
</div>
|
|
458
|
+
);
|
|
459
|
+
tooltipValue = JSON.stringify(jsonData, null, 2);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// If parsing fails, show truncated raw value
|
|
464
|
+
const displayValue = typeof jsonValue === 'string' && jsonValue.length > 30
|
|
465
|
+
? jsonValue.substring(0, 30) + '...'
|
|
466
|
+
: jsonValue;
|
|
467
|
+
displayContent = (
|
|
468
|
+
<span style={{ fontSize: '0.75rem', fontFamily: 'monospace', color: 'var(--paragraph-color)' }}>
|
|
469
|
+
{displayValue}
|
|
470
|
+
</span>
|
|
471
|
+
);
|
|
472
|
+
tooltipValue = jsonValue;
|
|
473
|
+
}
|
|
379
474
|
|
|
380
475
|
return (
|
|
381
476
|
<CellWithTooltip
|
|
382
|
-
value={
|
|
477
|
+
value={tooltipValue}
|
|
383
478
|
columnType="json"
|
|
384
479
|
>
|
|
385
|
-
|
|
480
|
+
{displayContent}
|
|
386
481
|
</CellWithTooltip>
|
|
387
482
|
);
|
|
388
483
|
} else {
|