Skip to content

FORMS-25983 Add min/max cross-validation to authoring dialogs#1903

Open
AnurudraS wants to merge 3 commits into
masterfrom
FORMS-25983
Open

FORMS-25983 Add min/max cross-validation to authoring dialogs#1903
AnurudraS wants to merge 3 commits into
masterfrom
FORMS-25983

Conversation

@AnurudraS

Copy link
Copy Markdown
Contributor

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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes and the overall coverage did not decrease.
  • All unit tests pass on CircleCi.
  • I ran all tests locally and they pass.

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>
@AnurudraS AnurudraS requested a review from devgurjar June 11, 2026 09:40
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); };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 > 1false → 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with the fix

Comment on lines +347 to +348
minField.invalid = invalid;
maxField.invalid = invalid;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with the fix

Comment on lines +453 to +466
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);
}
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with the fix

Comment on lines +398 to +437
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"
}
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architecture / DRY — selectors duplicated, ownership inverted from existing pattern.

  1. Selectors duplicated. textinput/v1/editDialog.js:21-22 already defines TEXTINPUT_MAXLENGTH / TEXTINPUT_MINLENGTH as constants. The same BEM strings are now restated here as raw strings. Same for datepicker/v1/editDialog.js:27-28 (DATEPICKER_MINDATE / DATEPICKER_MAXDATE) and fileinput/v1/editDialog.js:20-23. Rename in one place → silent rot in the other.

  2. 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.), with Utils providing 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.js calls Utils.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.
  3. Inconsistent selector shape inside the registry. numberinput uses granite:class so its selector is just .cmp-adaptiveform-numberinput__minimum (targets the coral element directly). Every other entry uses wrapperClass and appends coral-numberinput or coral-datepicker. getCoralField papers 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).

  4. datetime selector uses attribute names ([name='./minimumDateTime']). Brittle: any future rename of the property propagates here. Prefer adding a granite:class / wrapperClass to datetime/v1/_cq_dialog/.content.xml (the existing convention) and selecting by that instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated with the fix

Comment on lines +411 to +416
{
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"
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.xml to use the same wrapperClass names so the single entry covers both versions.

Please confirm intent and either expand the registry or expand v3's dialog markup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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); };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@AnurudraS AnurudraS requested a review from devgurjar June 16, 2026 06:50
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants