@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.
@@ -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={placeholder ? placeholder : 'Select...'}
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
- if (
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
- if (
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 */