@visns-studio/visns-components 6.0.1 → 6.0.3
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 +1 -1
- package/src/components/DataGrid.jsx +624 -113
- package/src/components/Form.jsx +150 -15
- package/src/components/MultiSelect.jsx +30 -2
- package/src/components/styles/DataGrid.module.scss +22 -4
- package/src/components/styles/MultiSelect.module.scss +10 -10
- package/src/components/styles/global.css +14 -10
package/src/components/Form.jsx
CHANGED
|
@@ -1202,12 +1202,28 @@ function Form({
|
|
|
1202
1202
|
let validation = '';
|
|
1203
1203
|
|
|
1204
1204
|
if (['create', 'update'].includes(formType)) {
|
|
1205
|
+
// A group modal renders only the handful of fields it
|
|
1206
|
+
// names, so the rest of the form's required fields are
|
|
1207
|
+
// not on screen for the user to fill — validating them
|
|
1208
|
+
// would make the modal impossible to save
|
|
1209
|
+
const modalFieldIds =
|
|
1210
|
+
bulkEditMode &&
|
|
1211
|
+
bulkEditGroupData?.iconConfig?.formModal?.fields
|
|
1212
|
+
? new Set(
|
|
1213
|
+
bulkEditGroupData.iconConfig.formModal.fields
|
|
1214
|
+
)
|
|
1215
|
+
: null;
|
|
1216
|
+
|
|
1205
1217
|
fields.forEach((item) => {
|
|
1206
1218
|
// Skip createOnly fields if formType is 'update'
|
|
1207
1219
|
if (formType === 'update' && item.createOnly) {
|
|
1208
1220
|
return;
|
|
1209
1221
|
}
|
|
1210
1222
|
|
|
1223
|
+
if (modalFieldIds && !modalFieldIds.has(item.id)) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1211
1227
|
// Check if the item is required
|
|
1212
1228
|
if (item.required) {
|
|
1213
1229
|
// Find the corresponding Field component to check for auto values
|
|
@@ -1573,6 +1589,20 @@ function Form({
|
|
|
1573
1589
|
bulkEditGroupData.iconConfig.formModal;
|
|
1574
1590
|
url = formModalConfig.url || url;
|
|
1575
1591
|
method = formModalConfig.method || 'POST';
|
|
1592
|
+
|
|
1593
|
+
// `anchorRow` modals act on the group through
|
|
1594
|
+
// one of its real records (the endpoint reads
|
|
1595
|
+
// the rest of the group off that row), so the
|
|
1596
|
+
// id goes in the path like an ordinary update
|
|
1597
|
+
if (formModalConfig.anchorRow) {
|
|
1598
|
+
const anchorRow =
|
|
1599
|
+
bulkEditGroupData.groupRows?.[0];
|
|
1600
|
+
url += `/${
|
|
1601
|
+
anchorRow?.[
|
|
1602
|
+
formSettings.primaryKey || 'id'
|
|
1603
|
+
]
|
|
1604
|
+
}`;
|
|
1605
|
+
}
|
|
1576
1606
|
} else {
|
|
1577
1607
|
switch (formType) {
|
|
1578
1608
|
case 'create':
|
|
@@ -1853,16 +1883,39 @@ function Form({
|
|
|
1853
1883
|
|
|
1854
1884
|
// Add bulk edit data to payload if in bulk edit mode
|
|
1855
1885
|
if (bulkEditMode && bulkEditGroupData) {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1886
|
+
const formModalConfig =
|
|
1887
|
+
bulkEditGroupData.iconConfig.formModal;
|
|
1888
|
+
|
|
1889
|
+
if (formModalConfig?.anchorRow) {
|
|
1890
|
+
// A plain record update: send only what the
|
|
1891
|
+
// modal asked for, or the untouched blanks of
|
|
1892
|
+
// every other form field would wipe the row
|
|
1893
|
+
const allowed = new Set([
|
|
1894
|
+
...(formModalConfig.fields || []),
|
|
1895
|
+
...Object.keys(formModalConfig.data || {}),
|
|
1896
|
+
]);
|
|
1897
|
+
|
|
1898
|
+
payload = Object.keys(payload).reduce(
|
|
1899
|
+
(acc, key) => {
|
|
1900
|
+
if (allowed.has(key)) {
|
|
1901
|
+
acc[key] = payload[key];
|
|
1902
|
+
}
|
|
1903
|
+
return acc;
|
|
1904
|
+
},
|
|
1905
|
+
{ ...formModalConfig.data }
|
|
1906
|
+
);
|
|
1907
|
+
} else {
|
|
1908
|
+
payload = {
|
|
1909
|
+
...payload,
|
|
1910
|
+
bulkEdit: true,
|
|
1911
|
+
groupValue: bulkEditGroupData.groupValue,
|
|
1912
|
+
groupKey:
|
|
1913
|
+
formModalConfig?.groupKey ||
|
|
1914
|
+
ajaxSetting?.groupBy?.[0],
|
|
1915
|
+
rowIds: bulkEditGroupData.groupRowIds,
|
|
1916
|
+
...formModalConfig?.data,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1866
1919
|
}
|
|
1867
1920
|
|
|
1868
1921
|
const res = await CustomFetch(url, method, payload);
|
|
@@ -2416,13 +2469,83 @@ function Form({
|
|
|
2416
2469
|
toast.error('Failed to fetch data:', error);
|
|
2417
2470
|
}
|
|
2418
2471
|
} else {
|
|
2472
|
+
const formModalConfig =
|
|
2473
|
+
bulkEditGroupData?.iconConfig?.formModal || null;
|
|
2474
|
+
|
|
2475
|
+
// Anchor-row modals submit against one real record, and the
|
|
2476
|
+
// endpoint reads that record's current values back out of the
|
|
2477
|
+
// payload (adding a header means "these plus this new one"), so
|
|
2478
|
+
// seed from the anchor rather than from what the group shares.
|
|
2479
|
+
// `seedFields` adds rows values a field needs but does not submit
|
|
2480
|
+
// — e.g. the job id a conflict check excludes itself by.
|
|
2481
|
+
if (
|
|
2482
|
+
formModalConfig?.anchorRow &&
|
|
2483
|
+
bulkEditGroupData?.groupRows?.length
|
|
2484
|
+
) {
|
|
2485
|
+
const anchorRow = bulkEditGroupData.groupRows[0];
|
|
2486
|
+
const seedIds = [
|
|
2487
|
+
...(formModalConfig.fields || []),
|
|
2488
|
+
...(formModalConfig.seedFields || []),
|
|
2489
|
+
];
|
|
2490
|
+
|
|
2491
|
+
seedIds.forEach((id) => {
|
|
2492
|
+
const field = formSettings.fields.find((f) => f.id === id);
|
|
2493
|
+
const relationKey = field?.relation?.[0];
|
|
2494
|
+
const relationValue = relationKey
|
|
2495
|
+
? anchorRow[relationKey]
|
|
2496
|
+
: undefined;
|
|
2497
|
+
|
|
2498
|
+
if (Array.isArray(relationValue)) {
|
|
2499
|
+
// The modal represents the whole group, but each
|
|
2500
|
+
// member row may hold only its own slice of the
|
|
2501
|
+
// relation (a split member carries one header), so
|
|
2502
|
+
// seed the union across every row, deduped. The
|
|
2503
|
+
// grow endpoint ignores ids a member already holds.
|
|
2504
|
+
const keyOf = (entry) =>
|
|
2505
|
+
entry[field.relation_from || 'id'];
|
|
2506
|
+
const seen = new Set();
|
|
2507
|
+
|
|
2508
|
+
_formData[id] = bulkEditGroupData.groupRows
|
|
2509
|
+
.flatMap((row) =>
|
|
2510
|
+
Array.isArray(row[relationKey])
|
|
2511
|
+
? row[relationKey]
|
|
2512
|
+
: []
|
|
2513
|
+
)
|
|
2514
|
+
.filter((entry) => {
|
|
2515
|
+
if (seen.has(keyOf(entry))) {
|
|
2516
|
+
return false;
|
|
2517
|
+
}
|
|
2518
|
+
seen.add(keyOf(entry));
|
|
2519
|
+
return true;
|
|
2520
|
+
})
|
|
2521
|
+
.map((entry) => ({
|
|
2522
|
+
value: keyOf(entry),
|
|
2523
|
+
label:
|
|
2524
|
+
entry[field.relationName] ??
|
|
2525
|
+
entry.label ??
|
|
2526
|
+
entry.name,
|
|
2527
|
+
// Existing memberships are locked in the
|
|
2528
|
+
// modal — it can only add, never take away
|
|
2529
|
+
isFixed: true,
|
|
2530
|
+
}));
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
if (anchorRow[id] !== undefined) {
|
|
2535
|
+
_formData[id] = anchorRow[id];
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2538
|
+
|
|
2539
|
+
setFormData(_formData);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2419
2543
|
// Bulk edit opens without a record fetch (columnId is null), so
|
|
2420
2544
|
// seed each field with the value the group rows already share —
|
|
2421
2545
|
// otherwise saved values (e.g. an assigned user) render blank and
|
|
2422
2546
|
// required fields force a re-pick on every open.
|
|
2423
2547
|
if (bulkEditMode && bulkEditGroupData?.groupRows?.length) {
|
|
2424
|
-
const modalFields =
|
|
2425
|
-
bulkEditGroupData.iconConfig?.formModal?.fields || [];
|
|
2548
|
+
const modalFields = formModalConfig?.fields || [];
|
|
2426
2549
|
|
|
2427
2550
|
formSettings.fields.forEach((item) => {
|
|
2428
2551
|
const { id, type, parent } = item;
|
|
@@ -3418,6 +3541,20 @@ function Form({
|
|
|
3418
3541
|
formData
|
|
3419
3542
|
)
|
|
3420
3543
|
: false;
|
|
3544
|
+
// A group modal whose save means one
|
|
3545
|
+
// narrow thing (e.g. "add a header")
|
|
3546
|
+
// must not also offer the stage-
|
|
3547
|
+
// completing saveAnother, which would
|
|
3548
|
+
// fire against every row in the group
|
|
3549
|
+
const showSaveAnother =
|
|
3550
|
+
formSettings.update?.hasOwnProperty(
|
|
3551
|
+
'saveAnother'
|
|
3552
|
+
) &&
|
|
3553
|
+
!(
|
|
3554
|
+
bulkEditMode &&
|
|
3555
|
+
bulkEditGroupData?.iconConfig
|
|
3556
|
+
?.formModal?.hideSaveAnother
|
|
3557
|
+
);
|
|
3421
3558
|
|
|
3422
3559
|
return (
|
|
3423
3560
|
<>
|
|
@@ -3431,9 +3568,7 @@ function Form({
|
|
|
3431
3568
|
>
|
|
3432
3569
|
Save & Close
|
|
3433
3570
|
</button>
|
|
3434
|
-
{
|
|
3435
|
-
'saveAnother'
|
|
3436
|
-
) && (
|
|
3571
|
+
{showSaveAnother && (
|
|
3437
3572
|
<button
|
|
3438
3573
|
className={
|
|
3439
3574
|
styles.btn
|
|
@@ -64,6 +64,13 @@ const CustomMultiValue = (props) => {
|
|
|
64
64
|
|
|
65
65
|
// Custom MultiValueRemove component with proper removal handling
|
|
66
66
|
const CustomMultiValueRemove = (props) => {
|
|
67
|
+
// Fixed values cannot be deselected: render no × at all — the module
|
|
68
|
+
// SCSS forces `display` on the remove button with !important, so
|
|
69
|
+
// hiding it from the styles function is not enough.
|
|
70
|
+
if (props.data?.isFixed) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
const handleRemove = (e) => {
|
|
68
75
|
e.stopPropagation();
|
|
69
76
|
|
|
@@ -334,7 +341,27 @@ function MultiSelect({
|
|
|
334
341
|
action: action
|
|
335
342
|
});
|
|
336
343
|
}
|
|
337
|
-
|
|
344
|
+
|
|
345
|
+
// Values seeded with isFixed represent memberships this
|
|
346
|
+
// field is not allowed to take away (e.g. a tag's existing
|
|
347
|
+
// headers in an add-only group modal): ignore their removal
|
|
348
|
+
// and survive a clear-all.
|
|
349
|
+
if (
|
|
350
|
+
['remove-value', 'pop-value'].includes(action.action) &&
|
|
351
|
+
action.removedValue?.isFixed
|
|
352
|
+
) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (action.action === 'clear') {
|
|
357
|
+
const fixed = selectValue.filter((v) => v?.isFixed);
|
|
358
|
+
|
|
359
|
+
if (fixed.length) {
|
|
360
|
+
onChange(fixed, action, settings.id);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
338
365
|
// Special handling for limit=1: keep only the most recent selection
|
|
339
366
|
if (
|
|
340
367
|
settings.limit === 1 &&
|
|
@@ -435,7 +462,8 @@ function MultiSelect({
|
|
|
435
462
|
multiValueRemove: (base, state) => ({
|
|
436
463
|
...base,
|
|
437
464
|
padding: '0 4px',
|
|
438
|
-
|
|
465
|
+
// Fixed values cannot be deselected, so they get no ×
|
|
466
|
+
display: state.data?.isFixed ? 'none' : 'flex',
|
|
439
467
|
alignItems: 'center',
|
|
440
468
|
justifyContent: 'center',
|
|
441
469
|
margin: '0',
|
|
@@ -357,7 +357,7 @@
|
|
|
357
357
|
|
|
358
358
|
/* Add custom styles to override react-grid-gallery */
|
|
359
359
|
:global {
|
|
360
|
-
|
|
360
|
+
// These styles will be applied globally
|
|
361
361
|
.ReactGridGallery_tile {
|
|
362
362
|
border-radius: 6px !important;
|
|
363
363
|
overflow: hidden !important;
|
|
@@ -508,7 +508,7 @@
|
|
|
508
508
|
|
|
509
509
|
/* Date field styling for uniform width */
|
|
510
510
|
:global {
|
|
511
|
-
|
|
511
|
+
// Target date cells specifically
|
|
512
512
|
.InovuaReactDataGrid__cell[data-column-id*='date'],
|
|
513
513
|
.InovuaReactDataGrid__cell[data-column-id*='Date'] {
|
|
514
514
|
min-width: 120px !important;
|
|
@@ -516,7 +516,7 @@
|
|
|
516
516
|
max-width: 120px !important;
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
|
|
519
|
+
// Target datetime cells specifically
|
|
520
520
|
.InovuaReactDataGrid__cell[data-column-id*='datetime'],
|
|
521
521
|
.InovuaReactDataGrid__cell[data-column-id*='Datetime'] {
|
|
522
522
|
min-width: 160px !important;
|
|
@@ -524,7 +524,7 @@
|
|
|
524
524
|
max-width: 160px !important;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
|
|
527
|
+
// Target daterange cells specifically
|
|
528
528
|
.InovuaReactDataGrid__cell[data-column-id*='daterange'] {
|
|
529
529
|
min-width: 200px !important;
|
|
530
530
|
width: 200px !important;
|
|
@@ -1048,6 +1048,24 @@
|
|
|
1048
1048
|
margin-left: 4px;
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
+
// treeGroupBy parent rows: the group's controls live in an ordinary grid
|
|
1052
|
+
// cell, so keep the buttons on one line and let the label take what is left
|
|
1053
|
+
.treeParentHeader {
|
|
1054
|
+
display: flex;
|
|
1055
|
+
align-items: center;
|
|
1056
|
+
gap: 2px;
|
|
1057
|
+
width: 100%;
|
|
1058
|
+
overflow: hidden;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.treeParentLabel {
|
|
1062
|
+
font-weight: 700;
|
|
1063
|
+
white-space: nowrap;
|
|
1064
|
+
overflow: hidden;
|
|
1065
|
+
text-overflow: ellipsis;
|
|
1066
|
+
margin-right: 6px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1051
1069
|
.groupValue {
|
|
1052
1070
|
font-size: 0.85rem;
|
|
1053
1071
|
color: var(--secondary-color, #1f2937);
|
|
@@ -91,14 +91,14 @@
|
|
|
91
91
|
overflow-y: auto !important;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// Control styles
|
|
95
95
|
.visns-select__control {
|
|
96
96
|
min-height: 50px !important;
|
|
97
97
|
height: auto !important; /* Allow height to grow with content */
|
|
98
98
|
overflow: visible !important;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// Value container styles
|
|
102
102
|
.visns-select__value-container {
|
|
103
103
|
padding: 2px 8px !important;
|
|
104
104
|
flex-wrap: wrap !important;
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
overflow-y: auto !important; /* Add scrolling when needed */
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
// Indicator styles
|
|
110
110
|
.visns-select__indicators {
|
|
111
111
|
display: flex !important;
|
|
112
112
|
align-items: center !important;
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
margin-left: 2px !important;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
// Multi-value styles
|
|
134
134
|
.visns-select__multi-value {
|
|
135
135
|
margin: 2px !important;
|
|
136
136
|
padding: 2px 4px !important;
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
transform: scale(0.95) !important;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// Fix for modals
|
|
191
191
|
.popup-content {
|
|
192
192
|
overflow: visible !important;
|
|
193
193
|
}
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
overflow: visible !important;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
// CSS class fixes for React Select
|
|
200
200
|
.css-13cymwt-control,
|
|
201
201
|
.css-t3ipsp-control {
|
|
202
202
|
min-height: 50px !important;
|
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
overflow: visible !important;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
|
|
207
|
+
// Option styling for description
|
|
208
208
|
.visns-select__option {
|
|
209
209
|
padding: 8px 12px !important;
|
|
210
210
|
}
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
overflow-y: auto !important;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
// Indicators container
|
|
220
220
|
.css-1hb7zxy-IndicatorsContainer {
|
|
221
221
|
display: flex !important;
|
|
222
222
|
align-items: center !important;
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
height: auto !important;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
// Individual indicators
|
|
228
228
|
.css-1xc3v61-indicatorContainer,
|
|
229
229
|
.css-15lsz6c-indicatorContainer {
|
|
230
230
|
padding: 8px !important;
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
align-self: stretch !important;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
// Separator
|
|
237
237
|
.css-1u9des2-indicatorSeparator {
|
|
238
238
|
align-self: stretch !important;
|
|
239
239
|
margin: 4px 0 !important;
|
|
@@ -316,18 +316,22 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
|
316
316
|
border-width: 1px;
|
|
317
317
|
box-sizing: border-box;
|
|
318
318
|
box-shadow: none !important;
|
|
319
|
+
}
|
|
319
320
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
/* These were written as `&--is-focused` / `&--menu-is-open` nested inside the
|
|
322
|
+
rule above. That is Sass syntax — this is a plain .css file, and CSS nesting
|
|
323
|
+
cannot concatenate a suffix onto the parent selector, so both rules were
|
|
324
|
+
invalid and the focus/menu-open border colour never applied. */
|
|
325
|
+
.visns-select__control--is-focused {
|
|
326
|
+
border-color: var(--primary-color) !important;
|
|
327
|
+
box-shadow: none !important;
|
|
328
|
+
border-width: 1px !important;
|
|
329
|
+
}
|
|
325
330
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
+
.visns-select__control--menu-is-open {
|
|
332
|
+
border-color: var(--primary-color) !important;
|
|
333
|
+
box-shadow: none !important;
|
|
334
|
+
border-width: 1px !important;
|
|
331
335
|
}
|
|
332
336
|
|
|
333
337
|
.visns-select__menu {
|