@visns-studio/visns-components 5.15.10 → 5.15.12
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/package.json +10 -10
- package/src/components/Breadcrumb.jsx +19 -11
- package/src/components/DataGrid.jsx +0 -5
- package/src/components/Field.jsx +58 -8
- package/src/components/Form.jsx +263 -101
- package/src/components/MultiCheckbox.jsx +335 -0
- package/src/components/MultiSelect.jsx +30 -1
- package/src/components/columns/ColumnRenderers.jsx +2 -38
- package/src/components/generic/GenericDetail.jsx +0 -10
- package/src/components/oauth/DataPreviewModal.jsx +372 -0
- package/src/components/oauth/DataSyncWizard.jsx +850 -0
- package/src/components/oauth/OAuthConnectionStatus.jsx +85 -0
- package/src/components/oauth/OAuthIntegrationsPage.jsx +185 -0
- package/src/components/oauth/OAuthManager.jsx +260 -0
- package/src/components/oauth/OAuthProviderCard.jsx +291 -0
- package/src/components/styles/DataPreviewModal.module.scss +658 -0
- package/src/components/styles/DataSyncWizard.module.scss +1211 -0
- package/src/components/styles/MultiCheckbox.module.scss +321 -0
- package/src/components/styles/OAuthConnectionStatus.module.scss +143 -0
- package/src/components/styles/OAuthManager.module.scss +119 -0
- package/src/components/styles/OAuthProviderCard.module.scss +760 -0
- package/src/index.js +14 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { Plus, Edit3, X, Check } from 'lucide-react';
|
|
3
|
+
import CustomFetch from './Fetch';
|
|
4
|
+
import { toast } from 'react-toastify';
|
|
5
|
+
import styles from './styles/MultiCheckbox.module.scss';
|
|
6
|
+
|
|
7
|
+
function MultiCheckbox({
|
|
8
|
+
className,
|
|
9
|
+
inputValue = [],
|
|
10
|
+
onChange,
|
|
11
|
+
options = [],
|
|
12
|
+
placeholder,
|
|
13
|
+
settings,
|
|
14
|
+
style,
|
|
15
|
+
isCreatable = false,
|
|
16
|
+
isEditable = false,
|
|
17
|
+
creatableConfig = {},
|
|
18
|
+
editableConfig = {},
|
|
19
|
+
}) {
|
|
20
|
+
const [checkboxOptions, setCheckboxOptions] = useState([]);
|
|
21
|
+
const [selectedValues, setSelectedValues] = useState([]);
|
|
22
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
23
|
+
const [isEditing, setIsEditing] = useState(null);
|
|
24
|
+
const [newTagName, setNewTagName] = useState('');
|
|
25
|
+
const [editTagName, setEditTagName] = useState('');
|
|
26
|
+
const prevInputValueRef = useRef();
|
|
27
|
+
|
|
28
|
+
// Transform options to checkbox format
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const _options = Array.isArray(options) && options.length > 0
|
|
31
|
+
? options.map((item) => {
|
|
32
|
+
if (item && typeof item === 'object') {
|
|
33
|
+
return {
|
|
34
|
+
id: item.id || 'unknown-id',
|
|
35
|
+
label: item.label || item.name || item.description || 'Unknown',
|
|
36
|
+
value: item.id || 'unknown-id',
|
|
37
|
+
...item,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
id: 'unknown-id',
|
|
42
|
+
label: 'Unknown',
|
|
43
|
+
value: 'unknown-id',
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
: [];
|
|
47
|
+
setCheckboxOptions(_options);
|
|
48
|
+
}, [options]);
|
|
49
|
+
|
|
50
|
+
// Transform inputValue to selected values
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (JSON.stringify(prevInputValueRef.current) !== JSON.stringify(inputValue)) {
|
|
53
|
+
const _selected = Array.isArray(inputValue)
|
|
54
|
+
? inputValue.map((item) => ({
|
|
55
|
+
id: item.id,
|
|
56
|
+
label: item.label || item.name || item.description || 'Unknown',
|
|
57
|
+
value: item.id,
|
|
58
|
+
...item,
|
|
59
|
+
}))
|
|
60
|
+
: inputValue && typeof inputValue === 'object'
|
|
61
|
+
? [{
|
|
62
|
+
id: inputValue.id,
|
|
63
|
+
label: inputValue.label || inputValue.name || inputValue.description || 'Unknown',
|
|
64
|
+
value: inputValue.id,
|
|
65
|
+
...inputValue,
|
|
66
|
+
}]
|
|
67
|
+
: [];
|
|
68
|
+
|
|
69
|
+
setSelectedValues(_selected);
|
|
70
|
+
prevInputValueRef.current = inputValue;
|
|
71
|
+
}
|
|
72
|
+
}, [inputValue, checkboxOptions]);
|
|
73
|
+
|
|
74
|
+
// Use all options since search is removed
|
|
75
|
+
const filteredOptions = checkboxOptions;
|
|
76
|
+
|
|
77
|
+
const handleCheckboxChange = (option, isChecked) => {
|
|
78
|
+
let newSelected;
|
|
79
|
+
|
|
80
|
+
if (isChecked) {
|
|
81
|
+
// Add to selection
|
|
82
|
+
newSelected = [...selectedValues, option];
|
|
83
|
+
} else {
|
|
84
|
+
// Remove from selection
|
|
85
|
+
newSelected = selectedValues.filter(item => item.id !== option.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setSelectedValues(newSelected);
|
|
89
|
+
|
|
90
|
+
// Call parent onChange with the new selection
|
|
91
|
+
if (onChange) {
|
|
92
|
+
onChange(newSelected, { action: isChecked ? 'select-option' : 'deselect-option' }, settings.id);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleCreateTag = async () => {
|
|
97
|
+
if (!newTagName.trim() || !creatableConfig.url) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const response = await CustomFetch(
|
|
101
|
+
creatableConfig.url,
|
|
102
|
+
creatableConfig.method || 'POST',
|
|
103
|
+
{ label: newTagName.trim() }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (response && response.data) {
|
|
107
|
+
const newOption = {
|
|
108
|
+
id: response.data.id,
|
|
109
|
+
label: response.data.label || newTagName.trim(),
|
|
110
|
+
value: response.data.id,
|
|
111
|
+
...response.data,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Add to options
|
|
115
|
+
setCheckboxOptions(prev => [...prev, newOption]);
|
|
116
|
+
|
|
117
|
+
// Auto-select the new tag
|
|
118
|
+
const newSelected = [...selectedValues, newOption];
|
|
119
|
+
setSelectedValues(newSelected);
|
|
120
|
+
|
|
121
|
+
if (onChange) {
|
|
122
|
+
onChange(newSelected, { action: 'create-option' }, settings.id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setNewTagName('');
|
|
126
|
+
setIsCreating(false);
|
|
127
|
+
toast.success('Tag created successfully');
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
toast.error('Failed to create tag');
|
|
131
|
+
console.error('Error creating tag:', error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handleEditTag = async (tagId) => {
|
|
136
|
+
if (!editTagName.trim() || !editableConfig.url) return;
|
|
137
|
+
|
|
138
|
+
console.log('Editing tag:', { tagId, editableConfig, editTagName });
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const url = editableConfig.url.replace('{id}', tagId);
|
|
142
|
+
console.log('Edit URL:', url);
|
|
143
|
+
|
|
144
|
+
const response = await CustomFetch(
|
|
145
|
+
url,
|
|
146
|
+
editableConfig.method || 'PUT',
|
|
147
|
+
{ label: editTagName.trim() }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (response && response.data) {
|
|
151
|
+
// Update in options
|
|
152
|
+
setCheckboxOptions(prev =>
|
|
153
|
+
prev.map(opt =>
|
|
154
|
+
opt.id === tagId
|
|
155
|
+
? { ...opt, label: response.data.label || editTagName.trim() }
|
|
156
|
+
: opt
|
|
157
|
+
)
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Update in selected values
|
|
161
|
+
setSelectedValues(prev =>
|
|
162
|
+
prev.map(sel =>
|
|
163
|
+
sel.id === tagId
|
|
164
|
+
? { ...sel, label: response.data.label || editTagName.trim() }
|
|
165
|
+
: sel
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
setEditTagName('');
|
|
170
|
+
setIsEditing(null);
|
|
171
|
+
toast.success('Tag updated successfully');
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
toast.error('Failed to update tag');
|
|
175
|
+
console.error('Error updating tag:', error);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isSelected = (optionId) => {
|
|
180
|
+
return selectedValues.some(selected => selected.id === optionId);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const maxColumns = settings.maxColumns || 3;
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={`${styles.multiCheckbox} ${className || ''}`} style={style}>
|
|
187
|
+
|
|
188
|
+
{/* Create new tag section */}
|
|
189
|
+
{isCreatable && (
|
|
190
|
+
<div className={styles.createSection}>
|
|
191
|
+
{!isCreating ? (
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => setIsCreating(true)}
|
|
195
|
+
className={styles.createButton}
|
|
196
|
+
>
|
|
197
|
+
<Plus size={16} />
|
|
198
|
+
Create New Tag
|
|
199
|
+
</button>
|
|
200
|
+
) : (
|
|
201
|
+
<div className={styles.createForm}>
|
|
202
|
+
<input
|
|
203
|
+
type="text"
|
|
204
|
+
placeholder="Enter tag name..."
|
|
205
|
+
value={newTagName}
|
|
206
|
+
onChange={(e) => setNewTagName(e.target.value)}
|
|
207
|
+
onKeyDown={(e) => {
|
|
208
|
+
if (e.key === 'Enter') {
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
handleCreateTag();
|
|
211
|
+
} else if (e.key === 'Escape') {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
setIsCreating(false);
|
|
214
|
+
setNewTagName('');
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
className={styles.createInput}
|
|
218
|
+
autoFocus
|
|
219
|
+
/>
|
|
220
|
+
<button
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={handleCreateTag}
|
|
223
|
+
className={styles.saveButton}
|
|
224
|
+
disabled={!newTagName.trim()}
|
|
225
|
+
title="Save tag"
|
|
226
|
+
>
|
|
227
|
+
<Check size={16} />
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
setIsCreating(false);
|
|
233
|
+
setNewTagName('');
|
|
234
|
+
}}
|
|
235
|
+
className={styles.cancelButton}
|
|
236
|
+
>
|
|
237
|
+
<X size={16} />
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
{/* Checkbox grid */}
|
|
245
|
+
<div
|
|
246
|
+
className={styles.checkboxGrid}
|
|
247
|
+
style={{
|
|
248
|
+
gridTemplateColumns: `repeat(${maxColumns}, 1fr)`
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{filteredOptions.map((option, index) => (
|
|
252
|
+
<div key={option.id || `option-${index}`} className={styles.checkboxItem}>
|
|
253
|
+
{isEditing === option.id ? (
|
|
254
|
+
<div className={styles.editForm}>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
value={editTagName}
|
|
258
|
+
onChange={(e) => setEditTagName(e.target.value)}
|
|
259
|
+
onKeyPress={(e) => {
|
|
260
|
+
if (e.key === 'Enter') {
|
|
261
|
+
handleEditTag(option.id);
|
|
262
|
+
} else if (e.key === 'Escape') {
|
|
263
|
+
setIsEditing(null);
|
|
264
|
+
setEditTagName('');
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
className={styles.editInput}
|
|
268
|
+
autoFocus
|
|
269
|
+
/>
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
onClick={() => handleEditTag(option.id)}
|
|
273
|
+
className={styles.saveButton}
|
|
274
|
+
disabled={!editTagName.trim()}
|
|
275
|
+
title="Save changes"
|
|
276
|
+
>
|
|
277
|
+
<Check size={16} />
|
|
278
|
+
</button>
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
onClick={() => {
|
|
282
|
+
setIsEditing(null);
|
|
283
|
+
setEditTagName('');
|
|
284
|
+
}}
|
|
285
|
+
className={styles.cancelButton}
|
|
286
|
+
>
|
|
287
|
+
<X size={16} />
|
|
288
|
+
</button>
|
|
289
|
+
</div>
|
|
290
|
+
) : (
|
|
291
|
+
<label className={styles.checkboxLabel}>
|
|
292
|
+
<input
|
|
293
|
+
type="checkbox"
|
|
294
|
+
checked={isSelected(option.id)}
|
|
295
|
+
onChange={(e) => handleCheckboxChange(option, e.target.checked)}
|
|
296
|
+
className={styles.checkbox}
|
|
297
|
+
/>
|
|
298
|
+
<span className={styles.labelText}>{option.label}</span>
|
|
299
|
+
{isEditable && (
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={(e) => {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
console.log('Starting edit for option:', option);
|
|
305
|
+
setIsEditing(option.id);
|
|
306
|
+
setEditTagName(option.label);
|
|
307
|
+
}}
|
|
308
|
+
className={styles.editButton}
|
|
309
|
+
>
|
|
310
|
+
<Edit3 size={12} />
|
|
311
|
+
</button>
|
|
312
|
+
)}
|
|
313
|
+
</label>
|
|
314
|
+
)}
|
|
315
|
+
</div>
|
|
316
|
+
))}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* No options message */}
|
|
320
|
+
{filteredOptions.length === 0 && (
|
|
321
|
+
<div className={styles.noOptions}>
|
|
322
|
+
No tags available
|
|
323
|
+
{isCreatable && (
|
|
324
|
+
<div style={{ marginTop: '8px', fontSize: '12px', color: '#64748b' }}>
|
|
325
|
+
Click "Create New Tag" above to add your first tag
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export default MultiCheckbox;
|
|
@@ -112,6 +112,27 @@ const CustomMultiValueRemove = (props) => {
|
|
|
112
112
|
);
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
+
// Custom NoOptionsMessage component to guide users about creation capability
|
|
116
|
+
const CustomNoOptionsMessage = (props) => {
|
|
117
|
+
const { isCreatable, inputValue } = props.selectProps;
|
|
118
|
+
|
|
119
|
+
if (isCreatable && inputValue) {
|
|
120
|
+
return (
|
|
121
|
+
<components.NoOptionsMessage {...props}>
|
|
122
|
+
<div style={{ textAlign: 'center', padding: '8px' }}>
|
|
123
|
+
Press Enter to create "<strong>{inputValue}</strong>"
|
|
124
|
+
</div>
|
|
125
|
+
</components.NoOptionsMessage>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<components.NoOptionsMessage {...props}>
|
|
131
|
+
No options found
|
|
132
|
+
</components.NoOptionsMessage>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
115
136
|
function MultiSelect({
|
|
116
137
|
className,
|
|
117
138
|
inputValue = [], // Default to an empty array
|
|
@@ -275,12 +296,14 @@ function MultiSelect({
|
|
|
275
296
|
isClearable
|
|
276
297
|
isSearchable={isSearchable}
|
|
277
298
|
isMulti={multi}
|
|
299
|
+
isCreatable={isCreatable}
|
|
278
300
|
filterOption={createFilter({ ignoreAccents: false })}
|
|
279
301
|
components={{
|
|
280
302
|
SelectList,
|
|
281
303
|
Option: CustomOption,
|
|
282
304
|
MultiValue: (props) => <CustomMultiValue {...props} settings={settings} />,
|
|
283
305
|
MultiValueRemove: CustomMultiValueRemove,
|
|
306
|
+
NoOptionsMessage: CustomNoOptionsMessage,
|
|
284
307
|
}}
|
|
285
308
|
options={selectOptions}
|
|
286
309
|
menuPortalTarget={document.body}
|
|
@@ -411,7 +434,13 @@ function MultiSelect({
|
|
|
411
434
|
}}
|
|
412
435
|
value={selectValue}
|
|
413
436
|
onCreateOption={handleCreate}
|
|
414
|
-
placeholder={
|
|
437
|
+
placeholder={
|
|
438
|
+
placeholder
|
|
439
|
+
? placeholder
|
|
440
|
+
: isCreatable
|
|
441
|
+
? 'Select or type to create new...'
|
|
442
|
+
: 'Select...'
|
|
443
|
+
}
|
|
415
444
|
isOptionDisabled={
|
|
416
445
|
settings.limit && multi && settings.limit > 1
|
|
417
446
|
? () => selectValue.length >= settings.limit
|
|
@@ -1255,25 +1255,7 @@ export const renderRelationColumn = ({
|
|
|
1255
1255
|
);
|
|
1256
1256
|
|
|
1257
1257
|
// Log sorting analysis in development
|
|
1258
|
-
|
|
1259
|
-
intelligentSortingConfig.logAnalysis &&
|
|
1260
|
-
process.env.NODE_ENV === 'development'
|
|
1261
|
-
) {
|
|
1262
|
-
const relationshipPath = getRelationshipPath(column);
|
|
1263
|
-
console.log(
|
|
1264
|
-
`Relationship sorting analysis for "${relationshipPath}":`,
|
|
1265
|
-
{
|
|
1266
|
-
sortable: isSortable,
|
|
1267
|
-
fieldName: column.nameFrom,
|
|
1268
|
-
relationshipDepth: Array.isArray(column.id)
|
|
1269
|
-
? column.id.length
|
|
1270
|
-
: 1,
|
|
1271
|
-
explicitSortable: column.hasOwnProperty('sortable')
|
|
1272
|
-
? column.sortable
|
|
1273
|
-
: 'not specified',
|
|
1274
|
-
}
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1258
|
+
// Debug logging removed for cleaner console output
|
|
1277
1259
|
|
|
1278
1260
|
return {
|
|
1279
1261
|
...commonProps,
|
|
@@ -1386,25 +1368,7 @@ export const renderRelationArrayColumn = ({
|
|
|
1386
1368
|
);
|
|
1387
1369
|
|
|
1388
1370
|
// Log sorting analysis in development
|
|
1389
|
-
|
|
1390
|
-
intelligentSortingConfig.logAnalysis &&
|
|
1391
|
-
process.env.NODE_ENV === 'development'
|
|
1392
|
-
) {
|
|
1393
|
-
const relationshipPath = getRelationshipPath(column);
|
|
1394
|
-
console.log(
|
|
1395
|
-
`Relationship array sorting analysis for "${relationshipPath}":`,
|
|
1396
|
-
{
|
|
1397
|
-
sortable: isSortable,
|
|
1398
|
-
fieldName: column.nameFrom,
|
|
1399
|
-
relationshipDepth: Array.isArray(column.id)
|
|
1400
|
-
? column.id.length
|
|
1401
|
-
: 1,
|
|
1402
|
-
explicitSortable: column.hasOwnProperty('sortable')
|
|
1403
|
-
? column.sortable
|
|
1404
|
-
: 'not specified',
|
|
1405
|
-
}
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1371
|
+
// Debug logging removed for cleaner console output
|
|
1408
1372
|
|
|
1409
1373
|
return {
|
|
1410
1374
|
...commonProps,
|
|
@@ -243,16 +243,6 @@ function GenericDetail({
|
|
|
243
243
|
/** Fileupload states */
|
|
244
244
|
const [files, setFiles] = useState([]);
|
|
245
245
|
|
|
246
|
-
// Debug logging for files state changes
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
console.log('GenericDetail: Files state changed:', {
|
|
249
|
-
files,
|
|
250
|
-
filesCount: files ? files.length : 0,
|
|
251
|
-
filesType: typeof files,
|
|
252
|
-
isArray: Array.isArray(files),
|
|
253
|
-
firstFile: files && files.length > 0 ? files[0] : null,
|
|
254
|
-
});
|
|
255
|
-
}, [files]);
|
|
256
246
|
const [loadingProgress, setLoadingProgress] = useState(0);
|
|
257
247
|
|
|
258
248
|
/** General States */
|