@visns-studio/visns-components 5.11.2 → 5.11.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.
- package/README.md +55 -0
- package/package.json +1 -1
- package/src/components/crm/AssociationManager.jsx +431 -0
- package/src/components/crm/BusinessCardOcr.jsx +190 -6
- package/src/components/crm/ClientAssociationManager.jsx +362 -0
- package/src/components/crm/MergeEntity.jsx +1049 -0
- package/src/components/crm/MultiSelect.jsx +14 -3
- package/src/components/crm/Navigation.jsx +67 -8
- package/src/components/crm/cells/CellWithTooltip.jsx +35 -28
- package/src/components/crm/generic/GenericDetail.jsx +25 -0
- package/src/components/crm/generic/GenericReport.jsx +1457 -434
- package/src/components/crm/generic/styles/GenericReport.module.scss +598 -24
- package/src/components/crm/styles/AssociationManager.module.scss +364 -0
- package/src/components/crm/styles/BusinessCardOcr.module.scss +362 -94
- package/src/components/crm/styles/ClientAssociationManager.module.scss +290 -0
- package/src/components/crm/styles/MergeEntity.module.scss +690 -0
- package/src/components/crm/styles/MultiSelect.module.scss +18 -0
- package/src/components/crm/styles/Navigation.module.scss +148 -16
- package/src/components/crm/styles/global-datagrid.css +9 -0
- package/src/index.js +6 -0
package/README.md
CHANGED
|
@@ -1650,6 +1650,61 @@ Modal components support vertical alignment configuration with the `verticalAlig
|
|
|
1650
1650
|
|
|
1651
1651
|
The ReactDataGrid's `defaultGroupBy` prop enables internal grouping functionality that creates collapsible group headers within the table structure. It does NOT create any external UI elements that need relocation, as all grouping controls are internal to the table component.
|
|
1652
1652
|
|
|
1653
|
+
### Navigation Component Icon Visibility Issues
|
|
1654
|
+
|
|
1655
|
+
#### Browser Rendering Optimization Fix
|
|
1656
|
+
|
|
1657
|
+
**Problem:** Navigation action icons (specifically anchor/Link elements) would disappear during page scroll in production environments. The issue only affected anchor tags while button elements remained visible. Notably, the problem disappeared when browser dev tools were open, indicating a browser rendering optimization conflict.
|
|
1658
|
+
|
|
1659
|
+
**Root Cause:** Firefox and other browsers apply aggressive rendering optimizations that can incorrectly cull elements from the paint tree. The combination of CSS `contain: layout` property with sticky positioning was causing anchor elements to be optimized out of view during scroll events.
|
|
1660
|
+
|
|
1661
|
+
**Solution Applied:**
|
|
1662
|
+
|
|
1663
|
+
1. **Removed problematic containment properties:**
|
|
1664
|
+
- Removed `contain: layout` from `.content-header` and action elements
|
|
1665
|
+
- This property was causing layout optimization conflicts
|
|
1666
|
+
|
|
1667
|
+
2. **Enhanced compositor layer creation:**
|
|
1668
|
+
- Changed `transform: translateZ(0)` to `transform: translateZ(0) translateX(0) translateY(0)`
|
|
1669
|
+
- Added `isolation: isolate` to force elements onto their own render layers
|
|
1670
|
+
- Added `backface-visibility: hidden` to prevent rendering glitches
|
|
1671
|
+
|
|
1672
|
+
3. **Improved z-index stacking:**
|
|
1673
|
+
- Elevated z-index values from 100-103 range to 1000-1003 range
|
|
1674
|
+
- Ensures action icons stay above sticky header's stacking context
|
|
1675
|
+
|
|
1676
|
+
**Files Modified:**
|
|
1677
|
+
- `src/components/crm/styles/Navigation.module.scss` (content-header and action styling)
|
|
1678
|
+
- Enhanced both regular and alternate theme action containers
|
|
1679
|
+
|
|
1680
|
+
#### Navigation Component Debug Mode
|
|
1681
|
+
|
|
1682
|
+
The Navigation component now includes optional debugging capabilities to help diagnose rendering issues:
|
|
1683
|
+
|
|
1684
|
+
**Enable Debug Mode:**
|
|
1685
|
+
```jsx
|
|
1686
|
+
<Navigation debugIcons={true} {...otherProps} />
|
|
1687
|
+
```
|
|
1688
|
+
|
|
1689
|
+
**Debug Features:**
|
|
1690
|
+
- **Visual indicators:** Red borders around anchor elements with 'A' labels, blue borders around buttons with 'B' labels
|
|
1691
|
+
- **Console logging:** Detailed scroll event logs showing element positions, visibility, z-index, transforms, and containment properties
|
|
1692
|
+
- **Real-time monitoring:** Tracks icon visibility changes during scroll events
|
|
1693
|
+
|
|
1694
|
+
**Debug CSS Classes:**
|
|
1695
|
+
The component includes `.debug-icons` styles that can be applied for visual debugging:
|
|
1696
|
+
```scss
|
|
1697
|
+
.debug-icons {
|
|
1698
|
+
a, button {
|
|
1699
|
+
border: 2px solid red !important;
|
|
1700
|
+
background: rgba(255, 0, 0, 0.1) !important;
|
|
1701
|
+
// Additional debug styling...
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
```
|
|
1705
|
+
|
|
1706
|
+
This fix ensures navigation icons remain visible during scroll across all browser environments and rendering contexts.
|
|
1707
|
+
|
|
1653
1708
|
## Support
|
|
1654
1709
|
|
|
1655
1710
|
For support, please contact the VISNS Studio team or open an issue on GitHub.
|
package/package.json
CHANGED
|
@@ -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.11.
|
|
90
|
+
"version": "5.11.4",
|
|
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,431 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { toast } from 'react-toastify';
|
|
3
|
+
import { Star, Trash2, Plus, Users, Calendar, AlertTriangle, X } from 'lucide-react';
|
|
4
|
+
import CustomFetch from './Fetch';
|
|
5
|
+
import MultiSelect from './MultiSelect';
|
|
6
|
+
import styles from './styles/AssociationManager.module.scss';
|
|
7
|
+
|
|
8
|
+
const AssociationManager = ({
|
|
9
|
+
endpoints,
|
|
10
|
+
config,
|
|
11
|
+
dataId,
|
|
12
|
+
onUpdate = () => {}
|
|
13
|
+
}) => {
|
|
14
|
+
const [associations, setAssociations] = useState([]);
|
|
15
|
+
const [availableOptions, setAvailableOptions] = useState([]);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
|
|
18
|
+
// Dynamic state based on config
|
|
19
|
+
const [newAssociation, setNewAssociation] = useState(() => {
|
|
20
|
+
const initialState = {};
|
|
21
|
+
|
|
22
|
+
// Set up dynamic fields based on config
|
|
23
|
+
if (config.fields) {
|
|
24
|
+
config.fields.forEach(field => {
|
|
25
|
+
initialState[field.id] = field.defaultValue || (field.type === 'boolean' ? false :
|
|
26
|
+
field.type === 'select' ? (field.options?.[0]?.value || '') :
|
|
27
|
+
field.type === 'multi-select' ? [] : '');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return initialState;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Replace placeholders in endpoint URLs and data
|
|
35
|
+
const processTemplate = (template, data = {}) => {
|
|
36
|
+
let processed = template;
|
|
37
|
+
|
|
38
|
+
// Replace {dataId} with current dataId
|
|
39
|
+
processed = processed.replace(/\{dataId\}/g, dataId);
|
|
40
|
+
|
|
41
|
+
// Replace any other placeholders from provided data
|
|
42
|
+
Object.keys(data).forEach(key => {
|
|
43
|
+
const placeholder = new RegExp(`\\{${key}\\}`, 'g');
|
|
44
|
+
processed = processed.replace(placeholder, data[key]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return processed;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Load existing associations
|
|
51
|
+
const loadAssociations = async () => {
|
|
52
|
+
if (!endpoints.list) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
const url = processTemplate(endpoints.list);
|
|
57
|
+
const method = endpoints.listMethod || 'GET';
|
|
58
|
+
const response = await CustomFetch(url, method, endpoints.listParams || {});
|
|
59
|
+
|
|
60
|
+
const dataPath = config.dataPath || 'data';
|
|
61
|
+
const associationsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response);
|
|
62
|
+
|
|
63
|
+
// Ensure associationsData is always an array
|
|
64
|
+
const validAssociations = Array.isArray(associationsData) ? associationsData : [];
|
|
65
|
+
setAssociations(validAssociations);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
toast.error(config.messages?.loadError || 'Failed to load associations');
|
|
68
|
+
console.error('Error loading associations:', error);
|
|
69
|
+
setAssociations([]); // Set empty array on error
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Load available options for dropdown/select fields
|
|
76
|
+
const loadAvailableOptions = async () => {
|
|
77
|
+
if (!config.optionsEndpoint) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const url = processTemplate(config.optionsEndpoint.url);
|
|
81
|
+
const method = config.optionsEndpoint.method || 'POST';
|
|
82
|
+
const params = config.optionsEndpoint.params || {};
|
|
83
|
+
|
|
84
|
+
const response = await CustomFetch(url, method, params);
|
|
85
|
+
const dataPath = config.optionsEndpoint.dataPath || 'data';
|
|
86
|
+
const optionsData = dataPath.split('.').reduce((obj, key) => obj?.[key], response) || [];
|
|
87
|
+
|
|
88
|
+
setAvailableOptions(optionsData);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error('Failed to load options:', error);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Generic action handler
|
|
95
|
+
const handleAction = async (actionKey, data = {}) => {
|
|
96
|
+
const endpoint = endpoints[actionKey];
|
|
97
|
+
if (!endpoint) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const url = processTemplate(endpoint.url || endpoint, data);
|
|
101
|
+
const method = endpoint.method || 'POST';
|
|
102
|
+
const params = { ...endpoint.params, ...data };
|
|
103
|
+
|
|
104
|
+
await CustomFetch(url, method, params);
|
|
105
|
+
|
|
106
|
+
const successMessage = config.messages?.[`${actionKey}Success`] ||
|
|
107
|
+
endpoint.successMessage ||
|
|
108
|
+
'Action completed successfully';
|
|
109
|
+
toast.success(successMessage);
|
|
110
|
+
|
|
111
|
+
// Reset form if it's an associate action
|
|
112
|
+
if (actionKey === 'associate') {
|
|
113
|
+
const resetState = {};
|
|
114
|
+
if (config.fields) {
|
|
115
|
+
config.fields.forEach(field => {
|
|
116
|
+
resetState[field.id] = field.defaultValue || (field.type === 'boolean' ? false :
|
|
117
|
+
field.type === 'select' ? (field.options?.[0]?.value || '') :
|
|
118
|
+
field.type === 'multi-select' ? [] : '');
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
setNewAssociation(resetState);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
loadAssociations();
|
|
125
|
+
onUpdate();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const errorMessage = config.messages?.[`${actionKey}Error`] ||
|
|
128
|
+
endpoint.errorMessage ||
|
|
129
|
+
'Action failed';
|
|
130
|
+
toast.error(errorMessage);
|
|
131
|
+
console.error(error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Associate handler
|
|
136
|
+
const handleAssociate = async () => {
|
|
137
|
+
// Validate required fields
|
|
138
|
+
const requiredField = config.fields?.find(field => field.required);
|
|
139
|
+
const primaryField = config.fields?.find(field => field.isPrimary);
|
|
140
|
+
|
|
141
|
+
if (requiredField && !newAssociation[requiredField.id]) {
|
|
142
|
+
toast.error(`Please select a ${requiredField.label || requiredField.id}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Prepare data for submission
|
|
147
|
+
const submitData = { ...newAssociation };
|
|
148
|
+
|
|
149
|
+
// Handle complex field values (like objects from MultiSelect)
|
|
150
|
+
Object.keys(submitData).forEach(key => {
|
|
151
|
+
const value = submitData[key];
|
|
152
|
+
if (value && typeof value === 'object' && value.value !== undefined) {
|
|
153
|
+
submitData[key] = value.value;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await handleAction('associate', submitData);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Remove association handler
|
|
161
|
+
const handleDissociate = async (itemId) => {
|
|
162
|
+
const deleteField = config.deleteField || 'id';
|
|
163
|
+
await handleAction('dissociate', { [deleteField]: itemId });
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Set primary handler
|
|
167
|
+
const handleSetPrimary = async (itemId) => {
|
|
168
|
+
if (!endpoints.setPrimary) return;
|
|
169
|
+
|
|
170
|
+
const idField = config.primaryTargetField || config.deleteField || 'id';
|
|
171
|
+
|
|
172
|
+
await handleAction('setPrimary', { [idField]: itemId });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Remove primary handler
|
|
176
|
+
const handleRemovePrimary = async (itemId) => {
|
|
177
|
+
if (!endpoints.removePrimary) return;
|
|
178
|
+
|
|
179
|
+
const idField = config.primaryTargetField || config.deleteField || 'id';
|
|
180
|
+
|
|
181
|
+
await handleAction('removePrimary', { [idField]: itemId });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Clear all primary statuses (useful after merge)
|
|
185
|
+
const handleClearAllPrimary = async () => {
|
|
186
|
+
if (!endpoints.clearAllPrimary) return;
|
|
187
|
+
|
|
188
|
+
await handleAction('clearAllPrimary', { entity_id: dataId });
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Check if there are multiple primary associations
|
|
192
|
+
const hasMultiplePrimary = () => {
|
|
193
|
+
const primaryField = config.displayFields?.primary || 'is_primary';
|
|
194
|
+
return associations.filter(assoc => assoc[primaryField]).length > 1;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (dataId) {
|
|
199
|
+
loadAssociations();
|
|
200
|
+
loadAvailableOptions();
|
|
201
|
+
}
|
|
202
|
+
}, [dataId]);
|
|
203
|
+
|
|
204
|
+
if (loading) {
|
|
205
|
+
return <div className={styles.loading}>{config.messages?.loading || 'Loading associations...'}</div>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className={styles.container}>
|
|
210
|
+
<div className={styles.header}>
|
|
211
|
+
<h3>
|
|
212
|
+
<Users size={20} style={{ marginRight: '8px', verticalAlign: 'middle' }} />
|
|
213
|
+
{config.title || 'Associations'}
|
|
214
|
+
</h3>
|
|
215
|
+
<p>{config.description || 'Manage associations'}</p>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Multiple Primary Warning */}
|
|
219
|
+
{hasMultiplePrimary() && (
|
|
220
|
+
<div className={styles.multiplePrimaryWarning}>
|
|
221
|
+
<AlertTriangle size={16} style={{ marginRight: '8px' }} />
|
|
222
|
+
<div className={styles.warningContent}>
|
|
223
|
+
<strong>{config.messages?.multiplePrimaryWarning || 'Multiple Primary Associations Detected'}</strong>
|
|
224
|
+
<p>{config.messages?.multiplePrimaryDescription || 'You have multiple associations marked as primary. Please resolve this by keeping only one primary association.'}</p>
|
|
225
|
+
{endpoints.clearAllPrimary && (
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
className={styles.clearAllPrimaryBtn}
|
|
229
|
+
onClick={handleClearAllPrimary}
|
|
230
|
+
>
|
|
231
|
+
{config.labels?.clearAllPrimary || 'Clear All Primary Status'}
|
|
232
|
+
</button>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Existing Associations */}
|
|
239
|
+
<div className={styles.existingAssociations}>
|
|
240
|
+
<h4>{config.labels?.currentAssociations || 'Current Associations'}</h4>
|
|
241
|
+
{associations.length === 0 ? (
|
|
242
|
+
<div className={styles.noAssociations}>
|
|
243
|
+
{config.messages?.noAssociations || 'No associations found.'}
|
|
244
|
+
</div>
|
|
245
|
+
) : (
|
|
246
|
+
<div className={styles.associationsList}>
|
|
247
|
+
{associations.map((association) => {
|
|
248
|
+
const nameField = config.displayFields?.name || 'name';
|
|
249
|
+
const primaryField = config.displayFields?.primary || 'is_primary';
|
|
250
|
+
const relationshipField = config.displayFields?.relationship || 'relationship_type';
|
|
251
|
+
const notesField = config.displayFields?.notes || 'notes';
|
|
252
|
+
const dateField = config.displayFields?.date || 'associated_at';
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div
|
|
256
|
+
key={association.id}
|
|
257
|
+
className={`${styles.associationItem} ${association[primaryField] ? styles.primary : ''}`}
|
|
258
|
+
>
|
|
259
|
+
<div className={styles.associationInfo}>
|
|
260
|
+
<div className={styles.entityName}>
|
|
261
|
+
{association[nameField]}
|
|
262
|
+
{association[primaryField] && (
|
|
263
|
+
<span className={styles.primaryBadge}>
|
|
264
|
+
<Star size={12} style={{ marginRight: '4px' }} />
|
|
265
|
+
{config.labels?.primaryBadge || 'Primary'}
|
|
266
|
+
</span>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
{(association[relationshipField] || association[notesField]) && (
|
|
270
|
+
<div className={styles.relationshipInfo}>
|
|
271
|
+
{association[relationshipField] && (
|
|
272
|
+
<span className={styles.relationshipType}>
|
|
273
|
+
{association[relationshipField]}
|
|
274
|
+
</span>
|
|
275
|
+
)}
|
|
276
|
+
{association[notesField] && (
|
|
277
|
+
<span className={styles.notes}>
|
|
278
|
+
{association[relationshipField] ? ' - ' : ''}{association[notesField]}
|
|
279
|
+
</span>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
{dateField && association[dateField] && (
|
|
284
|
+
<div className={styles.associatedAt}>
|
|
285
|
+
<Calendar size={12} style={{ marginRight: '4px' }} />
|
|
286
|
+
{config.labels?.associatedAt || 'Associated'}: {new Date(association[dateField]).toLocaleDateString()}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
<div className={styles.associationActions}>
|
|
291
|
+
{!association[primaryField] && endpoints.setPrimary && (
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
className={styles.setPrimaryBtn}
|
|
295
|
+
onClick={() => handleSetPrimary(association.id)}
|
|
296
|
+
>
|
|
297
|
+
<Star size={14} style={{ marginRight: '4px' }} />
|
|
298
|
+
{config.labels?.setPrimary || 'Set Primary'}
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
{association[primaryField] && endpoints.removePrimary && (
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
className={styles.removePrimaryBtn}
|
|
305
|
+
onClick={() => handleRemovePrimary(association.id)}
|
|
306
|
+
title={config.labels?.removePrimaryTooltip || 'Remove primary status from this association'}
|
|
307
|
+
>
|
|
308
|
+
<X size={14} style={{ marginRight: '4px' }} />
|
|
309
|
+
{config.labels?.removePrimary || 'Remove Primary'}
|
|
310
|
+
</button>
|
|
311
|
+
)}
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
className={styles.removeBtn}
|
|
315
|
+
onClick={() => handleDissociate(association.id)}
|
|
316
|
+
>
|
|
317
|
+
<Trash2 size={14} style={{ marginRight: '4px' }} />
|
|
318
|
+
{config.labels?.remove || 'Remove'}
|
|
319
|
+
</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
})}
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{/* Add New Association */}
|
|
329
|
+
<div className={styles.newAssociation}>
|
|
330
|
+
<h4>{config.labels?.addNew || 'Add New Association'}</h4>
|
|
331
|
+
<div className={styles.associationForm}>
|
|
332
|
+
{config.fields && config.fields.map((field, index) => {
|
|
333
|
+
const fieldValue = newAssociation[field.id] || '';
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div key={field.id} className={styles.formRow}>
|
|
337
|
+
<div className={styles.formField}>
|
|
338
|
+
<label>
|
|
339
|
+
{field.label}{field.required && ' *'}
|
|
340
|
+
</label>
|
|
341
|
+
|
|
342
|
+
{field.type === 'multi-select' && (
|
|
343
|
+
<MultiSelect
|
|
344
|
+
settings={{ id: field.id }}
|
|
345
|
+
multi={field.multi || false}
|
|
346
|
+
onChange={(value) => setNewAssociation(prev => ({ ...prev, [field.id]: value }))}
|
|
347
|
+
inputValue={fieldValue}
|
|
348
|
+
options={availableOptions}
|
|
349
|
+
placeholder={field.placeholder || `Select ${field.label}...`}
|
|
350
|
+
/>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{field.type === 'select' && (
|
|
354
|
+
<select
|
|
355
|
+
value={fieldValue}
|
|
356
|
+
onChange={(e) => setNewAssociation(prev => ({
|
|
357
|
+
...prev,
|
|
358
|
+
[field.id]: e.target.value
|
|
359
|
+
}))}
|
|
360
|
+
className={styles.selectField}
|
|
361
|
+
>
|
|
362
|
+
{field.options && field.options.map(option => (
|
|
363
|
+
<option key={option.value || option} value={option.value || option}>
|
|
364
|
+
{option.label || option}
|
|
365
|
+
</option>
|
|
366
|
+
))}
|
|
367
|
+
</select>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{field.type === 'boolean' && (
|
|
371
|
+
<label>
|
|
372
|
+
<input
|
|
373
|
+
type="checkbox"
|
|
374
|
+
checked={fieldValue}
|
|
375
|
+
onChange={(e) => setNewAssociation(prev => ({
|
|
376
|
+
...prev,
|
|
377
|
+
[field.id]: e.target.checked
|
|
378
|
+
}))}
|
|
379
|
+
/>
|
|
380
|
+
{field.checkboxLabel || field.label}
|
|
381
|
+
</label>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
{field.type === 'textarea' && (
|
|
385
|
+
<textarea
|
|
386
|
+
value={fieldValue}
|
|
387
|
+
onChange={(e) => setNewAssociation(prev => ({
|
|
388
|
+
...prev,
|
|
389
|
+
[field.id]: e.target.value
|
|
390
|
+
}))}
|
|
391
|
+
placeholder={field.placeholder || ''}
|
|
392
|
+
className={styles.textareaField}
|
|
393
|
+
rows={field.rows || 3}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{field.type === 'text' && (
|
|
398
|
+
<input
|
|
399
|
+
type="text"
|
|
400
|
+
value={fieldValue}
|
|
401
|
+
onChange={(e) => setNewAssociation(prev => ({
|
|
402
|
+
...prev,
|
|
403
|
+
[field.id]: e.target.value
|
|
404
|
+
}))}
|
|
405
|
+
placeholder={field.placeholder || ''}
|
|
406
|
+
className={styles.textField}
|
|
407
|
+
/>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
})}
|
|
413
|
+
|
|
414
|
+
<div className={styles.formActions}>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
className={styles.associateBtn}
|
|
418
|
+
onClick={handleAssociate}
|
|
419
|
+
disabled={!newAssociation[config.fields?.find(f => f.required)?.id || 'id']}
|
|
420
|
+
>
|
|
421
|
+
<Plus size={14} style={{ marginRight: '4px' }} />
|
|
422
|
+
{config.labels?.addButton || 'Add Association'}
|
|
423
|
+
</button>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
export default AssociationManager;
|