FORMS-25983 Add min/max cross-validation to authoring dialogs#1903
FORMS-25983 Add min/max cross-validation to authoring dialogs#1903AnurudraS wants to merge 3 commits into
Conversation
Prevent authors from setting a minimum value greater than its paired maximum (minLength/maxLength, minimum/maximum, minItems/maxItems, minimumDate/maximumDate, minimumDateTime/maximumDateTime, minOccur/maxOccur) across all affected form components. All pairs are registered centrally in Utils so new components only need one entry added — no per-component editDialog.js changes required. Real-time inline feedback via change listeners; dialog save blocked via foundation.validation.validator registered once at page load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| var minField = getCoralField(minSelector); | ||
| var maxField = getCoralField(maxSelector); | ||
| if (!minField || !maxField) return; | ||
| var compare = compareFn || function(a, b) { return parseInt(a, 10) > parseInt(b, 10); }; |
There was a problem hiding this comment.
Bug — parseInt will silently accept invalid decimal ranges in numberinput.
numberinput/v1/editDialog.js:66-68 sets step = 0.01 when the field type is number. With this default comparator, author entering min=1.6 / max=1.4 evaluates as parseInt("1.6",10) > parseInt("1.4",10) → 1 > 1 → false → the pair is accepted as valid.
The textinput length, fileinput count and panel occurrence pairs are genuinely integer-only, but the numeric numberinput pair needs parseFloat (or Number()). Cleanest fix: make this default explicitly integer (rename to INT_COMPARE), add a NUMBER_COMPARE = (a,b) => Number(a) > Number(b), and apply NUMBER_COMPARE on the numberinput entry in MIN_MAX_PAIRS (mirrors how DATE_COMPARE is already declared per-entry).
Same default is duplicated at line 372 inside registerMinMaxValidator — pull the default into a single module-scope DEFAULT_COMPARE to avoid drift.
There was a problem hiding this comment.
updated with the fix
| minField.invalid = invalid; | ||
| maxField.invalid = invalid; |
There was a problem hiding this comment.
Risk — this overwrites the invalid state set by other validators.
Force-assigning field.invalid = false whenever the cross-field relationship becomes valid will erase a required/pattern/etc. error that another validator just set on the same field. Example flow: author leaves minimum empty (foundation required flips invalid=true), then enters maximum=5 — the change listener fires, minVal is empty so invalid evaluates to false, and this line resets minimum.invalid to false, erasing the required-field error.
The registerMinMaxValidator foundation-validator (line 371) is the right channel for this — it composes correctly with other validators via the registry. Suggestion: drop the direct field.invalid = mutation in handleMinMaxValidation and instead trigger re-validation on both fields ($(minField).trigger("change") / foundation-validation framework call) so the framework re-runs all validators and reconciles invalid state.
There was a problem hiding this comment.
updated with the fix
| channel.on("foundation-contentloaded", function(e) { | ||
| var dialog = $(e.target); | ||
| MIN_MAX_PAIRS.forEach(function(pair) { | ||
| if (dialog.find(pair.minSelector).length && dialog.find(pair.maxSelector).length) { | ||
| Utils.handleMinMaxValidation( | ||
| pair.minSelector, | ||
| pair.maxSelector, | ||
| pair.minMsg, | ||
| pair.maxMsg, | ||
| pair.compareFn | ||
| )(e.target); | ||
| } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Missing Coral.commons.ready wrapping — fields may not be Coral-upgraded when this runs.
The existing canonical pattern in this same file (Utils.initializeEditDialog, line 117) wraps the dialog body in Coral.commons.ready(e.target, ...) before touching fields. Without it, minField.value / maxField.value are read from the raw input before coral-numberinput / coral-datepicker upgrades, so the initial validate() call on dialog open can misread values (especially for coral-datepicker, which exposes its parsed date only after upgrade).
Suggested shape:
channel.on("foundation-contentloaded", function(e) {
var dialog = $(e.target);
// early-exit if this fragment isn't a known min/max dialog
Coral.commons.ready(e.target, function() {
MIN_MAX_PAIRS.forEach(function(pair) { ... });
});
});Also: foundation-contentloaded fires for every parsed fragment, not just dialogs. Worth an early-exit (e.g. check for .cq-dialog-content in e.target) before iterating all 6 pairs × 2 find() calls per fire.
There was a problem hiding this comment.
updated with the fix
| var MIN_MAX_PAIRS = [ | ||
| { | ||
| minSelector: ".cmp-adaptiveform-textinput__minlength coral-numberinput", | ||
| maxSelector: ".cmp-adaptiveform-textinput__maxlength coral-numberinput", | ||
| minMsg: "Minimum length cannot be greater than maximum length", | ||
| maxMsg: "Maximum length cannot be less than minimum length" | ||
| }, | ||
| { | ||
| minSelector: ".cmp-adaptiveform-numberinput__minimum", | ||
| maxSelector: ".cmp-adaptiveform-numberinput__maximum", | ||
| minMsg: "Minimum value cannot be greater than maximum value", | ||
| maxMsg: "Maximum value cannot be less than minimum value" | ||
| }, | ||
| { | ||
| minSelector: ".cmp-adaptiveform-fileinput__minimumFiles coral-numberinput", | ||
| maxSelector: ".cmp-adaptiveform-fileinput__maximumFiles coral-numberinput", | ||
| minMsg: "Minimum files cannot be greater than maximum files", | ||
| maxMsg: "Maximum files cannot be less than minimum files" | ||
| }, | ||
| { | ||
| minSelector: ".cmp-adaptiveform-datepicker__mindate coral-datepicker", | ||
| maxSelector: ".cmp-adaptiveform-datepicker__maxdate coral-datepicker", | ||
| minMsg: "Minimum date cannot be after maximum date", | ||
| maxMsg: "Maximum date cannot be before minimum date", | ||
| compareFn: DATE_COMPARE | ||
| }, | ||
| { | ||
| minSelector: ".cmp-adaptiveform-datetime__editdialog coral-datepicker[name='./minimumDateTime']", | ||
| maxSelector: ".cmp-adaptiveform-datetime__editdialog coral-datepicker[name='./maximumDateTime']", | ||
| minMsg: "Minimum date-time cannot be after maximum date-time", | ||
| maxMsg: "Maximum date-time cannot be before minimum date-time", | ||
| compareFn: DATE_COMPARE | ||
| }, | ||
| { | ||
| minSelector: ".cmp-adaptiveform-panelcontainer__minOccur coral-numberinput", | ||
| maxSelector: ".cmp-adaptiveform-panelcontainer__maxOccur coral-numberinput", | ||
| minMsg: "Minimum occurrence cannot be greater than maximum occurrence", | ||
| maxMsg: "Maximum occurrence cannot be less than minimum occurrence" | ||
| } | ||
| ]; |
There was a problem hiding this comment.
Architecture / DRY — selectors duplicated, ownership inverted from existing pattern.
-
Selectors duplicated.
textinput/v1/editDialog.js:21-22already definesTEXTINPUT_MAXLENGTH/TEXTINPUT_MINLENGTHas constants. The same BEM strings are now restated here as raw strings. Same fordatepicker/v1/editDialog.js:27-28(DATEPICKER_MINDATE/DATEPICKER_MAXDATE) andfileinput/v1/editDialog.js:20-23. Rename in one place → silent rot in the other. -
Ownership inverted. Every other authoring-logic concern in this codebase lives in the component's own
editDialog.js(validation patterns, type-dependent step toggling, multifield prefill, etc.), withUtilsproviding the helpers they call. This PR moves component-specific selectors and copy into a shared file that has to be edited every time a new versioned component is added. Two patterns that better fit existing conventions:- Per-component registration. Each
editDialog.jscallsUtils.registerMinMaxValidator(MIN_CONST, MAX_CONST, ...)at module scope using its own existing selector constants. The util stays generic; selector strings live next to the dialog they describe. - Or: export the selector constants from each
editDialog.js/a shared per-component module so the central registry can reference them by name rather than restate the strings.
- Per-component registration. Each
-
Inconsistent selector shape inside the registry.
numberinputusesgranite:classso its selector is just.cmp-adaptiveform-numberinput__minimum(targets the coral element directly). Every other entry useswrapperClassand appendscoral-numberinputorcoral-datepicker.getCoralFieldpapers over this with a fallback, but the registry is harder to read than it needs to be — pick one shape and use it everywhere (the fallback helper goes away). -
datetimeselector uses attribute names ([name='./minimumDateTime']). Brittle: any future rename of the property propagates here. Prefer adding agranite:class/wrapperClasstodatetime/v1/_cq_dialog/.content.xml(the existing convention) and selecting by that instead.
There was a problem hiding this comment.
updated with the fix
| { | ||
| minSelector: ".cmp-adaptiveform-fileinput__minimumFiles coral-numberinput", | ||
| maxSelector: ".cmp-adaptiveform-fileinput__maximumFiles coral-numberinput", | ||
| minMsg: "Minimum files cannot be greater than maximum files", | ||
| maxMsg: "Maximum files cannot be less than minimum files" | ||
| }, |
There was a problem hiding this comment.
Coverage gap — fileinput/v3 is not covered.
ui.af.apps/.../form/fileinput/v3/fileinput/_cq_dialog/.content.xml exists in the repo but doesn't use the cmp-adaptiveform-fileinput__minimumFiles / __maximumFiles wrapperClass (a grep on the v3 dialog returns no hits). If v3 is the active version on newer projects, this entry is a no-op there and authors get no cross-validation.
Either:
- add a v3 entry with whatever selectors the v3 dialog uses, or
- update v3's
_cq_dialog/.content.xmlto use the samewrapperClassnames so the single entry covers both versions.
Please confirm intent and either expand the registry or expand v3's dialog markup.
There was a problem hiding this comment.
updated with the fix
| // Each entry describes one pair of min/max fields in an authoring dialog. | ||
| // To add validation for a new component, append an entry here — no changes | ||
| // needed in the component's own editDialog.js. | ||
| var DATE_COMPARE = function(a, b) { return new Date(a) > new Date(b); }; |
There was a problem hiding this comment.
Nit — invalid date strings are silently treated as valid.
var DATE_COMPARE = function(a, b) { return new Date(a) > new Date(b); };If a is "not-a-date" (e.g. a date-picker that's been cleared to a garbage string by a different validator path), new Date(a) is Invalid Date and every comparison against it returns false — so the pair is silently accepted as valid.
Guard it:
var DATE_COMPARE = function(a, b) {
var da = new Date(a), db = new Date(b);
return !isNaN(da) && !isNaN(db) && da > db;
};Not a blocker — the upstream coral-datepicker mostly prevents bad strings — but worth tightening since the comparator is the single point that decides validity.
Also — the new strings ("Minimum length cannot be greater than maximum length", etc.) are direct Granite.I18n.getMessage() keys. Consistent with existing usage in checkbox/v1/editDialog.js:119 and base/v1/editDialog.js:145, so OK, but 12 new translation strings is non-trivial — worth a heads-up to whoever owns the i18n dictionary if there's a workflow for that.
There was a problem hiding this comment.
updated with the fix
1. Fix parseInt bug for decimal ranges in numberinput — use NUMBER_COMPARE (Number()) instead of INT_COMPARE (parseInt()) so min=1.6 / max=1.4 is correctly flagged as invalid. 2. Remove direct field.invalid mutation from handleMinMaxValidation — trigger change events instead so the foundation-validation framework re-runs all validators and avoids overwriting required/pattern errors. 3. Wrap foundation-contentloaded handler in Coral.commons.ready so fields are fully upgraded before values are read; add early exit for non-dialog fragments. 4. Move per-component selector strings and registrations into each component's own editDialog.js (textinput, numberinput, fileinput/v1, datepicker, datetime) using that file's existing selector constants. utils.js now only hosts the panelcontainer entry, which is shared across accordion/wizard/tabsontop/verticaltabs/fragment (none have editDialog.js). 5. Fix DATE_COMPARE to guard against invalid date strings. 6. Add wrapperClass to datetime dialog XML for minimumDateTime/maximumDateTime fields, replacing the brittle attribute-name selector. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eview fixes 1. Pass jQuery-wrapped dialog instead of raw e.target to the handler — dialog.find() requires jQuery; passing e.target caused TypeError on every panelcontainer/tabsontop/accordion dialog open. 2. Revert validate() to direct invalid mutation — triggering change events in the listener caused infinite recursion because jQuery trigger fires native DOM events which re-enter the same addEventListener callback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent authors from setting a minimum value greater than its paired maximum (minLength/maxLength, minimum/maximum, minItems/maxItems, minimumDate/maximumDate, minimumDateTime/maximumDateTime, minOccur/maxOccur) across all affected form components.
All pairs are registered centrally in Utils so new components only need one entry added — no per-component editDialog.js changes required. Real-time inline feedback via change listeners; dialog save blocked via foundation.validation.validator registered once at page load.
Description
Related Issue
Motivation and Context
How Has This Been Tested?
Screenshots (if appropriate):
Types of changes
Checklist: