@visns-studio/visns-components 6.3.4 → 6.3.5
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
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.3.
|
|
94
|
+
"version": "6.3.5",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
package/src/components/Field.jsx
CHANGED
|
@@ -2182,6 +2182,11 @@ function Field({
|
|
|
2182
2182
|
isEditable={settings.isEditable ?? false}
|
|
2183
2183
|
creatableConfig={settings.creatableConfig || {}}
|
|
2184
2184
|
editableConfig={settings.editableConfig || {}}
|
|
2185
|
+
// The checkbox renderer previously ignored the field's
|
|
2186
|
+
// read-only state, leaving `readOnlyWhen` configs
|
|
2187
|
+
// claiming a lock the UI didn't enforce — the same gap
|
|
2188
|
+
// the radio renderers had.
|
|
2189
|
+
disabled={isFieldReadOnly}
|
|
2185
2190
|
style={style}
|
|
2186
2191
|
/>
|
|
2187
2192
|
);
|
package/src/components/Form.jsx
CHANGED
|
@@ -91,6 +91,56 @@ const evaluateDisableWhen = (rule, data) => {
|
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Field visibility rules (`show`).
|
|
96
|
+
*
|
|
97
|
+
* A field carries `show: [{ id: '<other field id>', <condition> }]`, meaning
|
|
98
|
+
* "when MY value satisfies this condition, reveal the named field; otherwise
|
|
99
|
+
* hide it". With `self: true` the roles swap: the condition is read from the
|
|
100
|
+
* NAMED field's value and applied to the field carrying the rule.
|
|
101
|
+
*
|
|
102
|
+
* Supported conditions:
|
|
103
|
+
*
|
|
104
|
+
* { value: [...] } the value equals one of the listed literals. What
|
|
105
|
+
* toggles ({ value: [true] }) and radios
|
|
106
|
+
* ({ value: [1, 2] }) use.
|
|
107
|
+
*
|
|
108
|
+
* { minSelected: n } the value is an array holding at least n entries.
|
|
109
|
+
* For the multi-* types (multi-checkbox-ajax,
|
|
110
|
+
* multi-dropdown-ajax, …) whose value is a list of
|
|
111
|
+
* selected options — e.g. reveal a pair of per-item
|
|
112
|
+
* quantity inputs only once two boxes are ticked.
|
|
113
|
+
*
|
|
114
|
+
* Rules are evaluated when the record loads (the effect keyed on
|
|
115
|
+
* `fetchTrigger`) and again on every change to the field that carries them,
|
|
116
|
+
* so a revealed field cannot linger after the value that revealed it goes
|
|
117
|
+
* away. See also `required_rely` (required-ness that follows another field's
|
|
118
|
+
* value) and `sumEquals` (a group of numbers that must total a target).
|
|
119
|
+
*/
|
|
120
|
+
const hasShowCondition = (condition) =>
|
|
121
|
+
!!condition &&
|
|
122
|
+
(condition.minSelected !== undefined ||
|
|
123
|
+
(Array.isArray(condition.value) && condition.value.length > 0));
|
|
124
|
+
|
|
125
|
+
const matchesShowCondition = (condition, value) => {
|
|
126
|
+
if (!condition) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (condition.minSelected !== undefined) {
|
|
131
|
+
return (
|
|
132
|
+
Array.isArray(value) &&
|
|
133
|
+
value.length >= Number(condition.minSelected)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (Array.isArray(condition.value)) {
|
|
138
|
+
return condition.value.includes(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
|
|
94
144
|
function Form({
|
|
95
145
|
ajaxSetting,
|
|
96
146
|
api,
|
|
@@ -465,18 +515,71 @@ function Form({
|
|
|
465
515
|
});
|
|
466
516
|
};
|
|
467
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Re-evaluate this field's `show` rules against the value it has just
|
|
520
|
+
* been given. The other two change handlers do this from the DOM
|
|
521
|
+
* node's `data-show`, which a select / multi-checkbox never emits, so
|
|
522
|
+
* without this a rule on one of those types would only ever run at
|
|
523
|
+
* load time — the reveal would not follow the user's clicks.
|
|
524
|
+
*/
|
|
525
|
+
const applyShowConditions = (newValue) => {
|
|
526
|
+
const sourceField = formSettings.fields.find((f) => f.id === id);
|
|
527
|
+
const conditions = sourceField?.show || [];
|
|
528
|
+
|
|
529
|
+
if (conditions.length === 0) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const _fields = [...formSettings.fields];
|
|
534
|
+
let changed = false;
|
|
535
|
+
|
|
536
|
+
conditions.forEach((showObject) => {
|
|
537
|
+
if (!showObject?.id || !hasShowCondition(showObject)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const targetIndex = _fields.findIndex(
|
|
542
|
+
(f) => f.id === showObject.id
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
if (targetIndex < 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const shouldShow = matchesShowCondition(showObject, newValue);
|
|
550
|
+
const target = showObject.self
|
|
551
|
+
? _fields.find((f) => f.id === id)
|
|
552
|
+
: _fields[targetIndex];
|
|
553
|
+
|
|
554
|
+
if (target) {
|
|
555
|
+
target.hide = !shouldShow;
|
|
556
|
+
changed = true;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (changed && updateForm) {
|
|
561
|
+
updateForm((prevState) => ({
|
|
562
|
+
...prevState,
|
|
563
|
+
fields: _fields,
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
468
568
|
// // Switch cases to handle different actions
|
|
469
569
|
switch (action.action) {
|
|
470
570
|
case 'select-option':
|
|
471
571
|
case 'create-option': // New case to handle create-option
|
|
472
572
|
case 'deselect-option': // Handle unchecking tags
|
|
473
573
|
handleSelectOption();
|
|
574
|
+
applyShowConditions(inputValue);
|
|
474
575
|
break;
|
|
475
576
|
case 'clear':
|
|
476
577
|
updateFormData({ [id]: '' });
|
|
578
|
+
applyShowConditions([]);
|
|
477
579
|
break;
|
|
478
580
|
case 'remove-value':
|
|
479
581
|
updateFormData({ [id]: inputValue });
|
|
582
|
+
applyShowConditions(inputValue);
|
|
480
583
|
break;
|
|
481
584
|
}
|
|
482
585
|
};
|
|
@@ -1303,6 +1406,83 @@ function Form({
|
|
|
1303
1406
|
_inputClass[item.id] = styles.inputError;
|
|
1304
1407
|
}
|
|
1305
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* `sumEquals`: a set of numeric fields that must add
|
|
1412
|
+
* up to a target.
|
|
1413
|
+
*
|
|
1414
|
+
* sumEquals: {
|
|
1415
|
+
* fields: ['qty_a', 'qty_b'],
|
|
1416
|
+
* equals: 'qty_total', // field id, or a number
|
|
1417
|
+
* message: '…' // optional override
|
|
1418
|
+
* }
|
|
1419
|
+
*
|
|
1420
|
+
* Declared once, on any one of the fields in the
|
|
1421
|
+
* group. Every named field is marked in error so the
|
|
1422
|
+
* user sees which inputs to reconcile, not just a
|
|
1423
|
+
* message.
|
|
1424
|
+
*
|
|
1425
|
+
* The rule is skipped while its own field is hidden,
|
|
1426
|
+
* so a group that only appears in a revealed section
|
|
1427
|
+
* (see the `show` documentation above) cannot block an
|
|
1428
|
+
* ordinary save. While visible it also stands in for
|
|
1429
|
+
* `required` on the whole group: a blank member is as
|
|
1430
|
+
* wrong as a wrong total.
|
|
1431
|
+
*/
|
|
1432
|
+
if (item.sumEquals && item.hide !== true) {
|
|
1433
|
+
const rule = item.sumEquals;
|
|
1434
|
+
const groupIds = Array.isArray(rule.fields)
|
|
1435
|
+
? rule.fields
|
|
1436
|
+
: [];
|
|
1437
|
+
|
|
1438
|
+
if (groupIds.length > 0) {
|
|
1439
|
+
const labelFor = (fieldId) =>
|
|
1440
|
+
fields.find((f) => f.id === fieldId)
|
|
1441
|
+
?.label || fieldId;
|
|
1442
|
+
const groupLabels = groupIds
|
|
1443
|
+
.map(labelFor)
|
|
1444
|
+
.join(' and ');
|
|
1445
|
+
const groupValues = groupIds.map(
|
|
1446
|
+
(fieldId) => formData[fieldId]
|
|
1447
|
+
);
|
|
1448
|
+
const incomplete = groupValues.some(
|
|
1449
|
+
(v) =>
|
|
1450
|
+
v === null ||
|
|
1451
|
+
v === undefined ||
|
|
1452
|
+
v === '' ||
|
|
1453
|
+
isNaN(Number(v))
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
const target =
|
|
1457
|
+
typeof rule.equals === 'number'
|
|
1458
|
+
? rule.equals
|
|
1459
|
+
: Number(formData[rule.equals]);
|
|
1460
|
+
|
|
1461
|
+
const markGroup = () =>
|
|
1462
|
+
groupIds.forEach((fieldId) => {
|
|
1463
|
+
_inputClass[fieldId] =
|
|
1464
|
+
styles.inputError;
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
if (incomplete) {
|
|
1468
|
+
validation += `${groupLabels} are required.<br/>`;
|
|
1469
|
+
markGroup();
|
|
1470
|
+
} else if (!isNaN(target)) {
|
|
1471
|
+
const sum = groupValues.reduce(
|
|
1472
|
+
(total, v) => total + Number(v),
|
|
1473
|
+
0
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
if (sum !== target) {
|
|
1477
|
+
validation +=
|
|
1478
|
+
(rule.message ||
|
|
1479
|
+
`${groupLabels} must add up to ${target} - they add up to ${sum}.`) +
|
|
1480
|
+
'<br/>';
|
|
1481
|
+
markGroup();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1306
1486
|
});
|
|
1307
1487
|
|
|
1308
1488
|
setInputClass(_inputClass);
|
|
@@ -2676,9 +2856,9 @@ function Form({
|
|
|
2676
2856
|
const showConditions = field.show || [];
|
|
2677
2857
|
|
|
2678
2858
|
showConditions.forEach((showObject) => {
|
|
2679
|
-
const { id,
|
|
2859
|
+
const { id, self } = showObject;
|
|
2680
2860
|
|
|
2681
|
-
if (id != '' &&
|
|
2861
|
+
if (id != '' && hasShowCondition(showObject)) {
|
|
2682
2862
|
const targetField = formSettings.fields.find(
|
|
2683
2863
|
(f) => f.id === id
|
|
2684
2864
|
);
|
|
@@ -2695,7 +2875,10 @@ function Form({
|
|
|
2695
2875
|
valueChecker !== undefined &&
|
|
2696
2876
|
valueChecker !== null
|
|
2697
2877
|
) {
|
|
2698
|
-
const shouldShow =
|
|
2878
|
+
const shouldShow = matchesShowCondition(
|
|
2879
|
+
showObject,
|
|
2880
|
+
valueChecker
|
|
2881
|
+
);
|
|
2699
2882
|
|
|
2700
2883
|
if (self) {
|
|
2701
2884
|
field.hide = !shouldShow;
|
|
@@ -16,6 +16,11 @@ function MultiCheckbox({
|
|
|
16
16
|
isEditable = false,
|
|
17
17
|
creatableConfig = {},
|
|
18
18
|
editableConfig = {},
|
|
19
|
+
// The checkbox renderer used to ignore the field's read-only state, so a
|
|
20
|
+
// `readOnlyWhen` config claimed a lock the UI never enforced (the same
|
|
21
|
+
// gap the radio renderers had). Disabling the boxes also takes the
|
|
22
|
+
// create/edit affordances away — they mutate the option list.
|
|
23
|
+
disabled = false,
|
|
19
24
|
}) {
|
|
20
25
|
const [checkboxOptions, setCheckboxOptions] = useState([]);
|
|
21
26
|
const [selectedValues, setSelectedValues] = useState([]);
|
|
@@ -75,8 +80,12 @@ function MultiCheckbox({
|
|
|
75
80
|
const filteredOptions = checkboxOptions;
|
|
76
81
|
|
|
77
82
|
const handleCheckboxChange = (option, isChecked) => {
|
|
83
|
+
if (disabled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
let newSelected;
|
|
79
|
-
|
|
88
|
+
|
|
80
89
|
if (isChecked) {
|
|
81
90
|
// Add to selection
|
|
82
91
|
newSelected = [...selectedValues, option];
|
|
@@ -186,7 +195,7 @@ function MultiCheckbox({
|
|
|
186
195
|
<div className={`${styles.multiCheckbox} ${className || ''}`} style={style}>
|
|
187
196
|
|
|
188
197
|
{/* Create new tag section */}
|
|
189
|
-
{isCreatable && (
|
|
198
|
+
{isCreatable && !disabled && (
|
|
190
199
|
<div className={styles.createSection}>
|
|
191
200
|
{!isCreating ? (
|
|
192
201
|
<button
|
|
@@ -294,9 +303,10 @@ function MultiCheckbox({
|
|
|
294
303
|
checked={isSelected(option.id)}
|
|
295
304
|
onChange={(e) => handleCheckboxChange(option, e.target.checked)}
|
|
296
305
|
className={styles.checkbox}
|
|
306
|
+
disabled={disabled}
|
|
297
307
|
/>
|
|
298
308
|
<span className={styles.labelText}>{option.label}</span>
|
|
299
|
-
{isEditable && (
|
|
309
|
+
{isEditable && !disabled && (
|
|
300
310
|
<button
|
|
301
311
|
type="button"
|
|
302
312
|
onClick={(e) => {
|