@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.4",
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": [
@@ -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
  );
@@ -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, value, self } = showObject;
2859
+ const { id, self } = showObject;
2680
2860
 
2681
- if (id != '' && value.length > 0) {
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 = value.includes(valueChecker);
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) => {