Skip to content

✨ Admin Portal: Voter editing quality of life features - #3078

Merged
Findeton merged 17 commits into
release/10.0from
feat/meta-12901/release/10.0
Aug 22, 2026
Merged

✨ Admin Portal: Voter editing quality of life features#3078
Findeton merged 17 commits into
release/10.0from
feat/meta-12901/release/10.0

Conversation

@Findeton

Copy link
Copy Markdown
Contributor

Findetonand others added 5 commits August 21, 2026 17:55
Parent issue: sequentech/meta#12901
Saving a voter closed the editor whatever happened and reported a fixed
"Error editing voter", so a rejected save discarded the admin's work and
told them nothing about why. The editor now stays open on failure and
shows the reason the backend gave, both inline next to the button that
failed and in a toast, with the raw rejection logged. A rejected password
reads the same way it does in the Edit Password dialog rather than
falling back to Harvest's untranslated text.
Creating a voter is split in two: a failed creation keeps the form open
to be retried, while a failure to set the password afterwards closes it,
because the voter already exists and submitting again would create a
second one. That case says so explicitly instead of claiming the
creation failed.
Both save controls now refuse a second click while one save is in flight.
SaveButton's alwaysEnable bypasses react-hook-form's isSubmitting
entirely, so keeping the form open on error had left the whole round trip
clickable, and the review step's Confirm is a plain button that
isSubmitting never covered at all.
Text fields honour a configured maximum. Keycloak caps typing with the
inputTypeMaxlength annotation and rejects on submit with the length
validator; the validator wins where an attribute carries both, since
capping at a lower typing hint would leave a stored value that already
exceeds it impossible to repair. The phone field is deliberately left
uncapped: it displays a formatted national number while it stores the
full international one, and intl-tel-input truncates its own value
against any maxlength it finds on the input.
Select options show their description next to the stored option, as
"M - Male", so the admin no longer has to interpret a bare code and can
still see what gets written to the voter. Descriptions come from the
inputOptionLabels annotation, which Keycloak's admin console stores as a
JSON string and the realm configuration as an object; indexing the
encoded form by an option would have read a character out of the JSON
text. The multiselect-checkboxes branch shares that decoding and now
resolves placeholder descriptions the same way.
Two defects surfaced on the way: the select passed a multivalued
attribute straight to Autocomplete, so its value never matched an option,
and it read attr.validations.options without guarding an attribute that
has no validations at all.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Saving a voter whose attribute broke a user profile constraint reported
"Error creating voter: not a valid json response from webhook". Two
faults stacked up.
create_user answered with a bare (Status, String), which Rocket writes as
a plain text body. Hasura could not map it to an action error, so it
replaced it with a message of its own and the real cause never left the
server. It now answers with the JsonError every other route uses, so the
reason reaches the admin portal at all.
The reason itself was also being thrown away. Keycloak reports a refused
attribute as a 400 whose body names the field, an i18n key for the
constraint and the constraint's arguments, e.g.
{"field":"roll","errorMessage":"error-invalid-length","params":["roll",1,2]}
The keycloak crate parses that into KeycloakHttpError, which keeps only
errorMessage and drops both the field and the arguments, so the raw body
is parsed instead. Several refused attributes arrive as a list and a
single one as a bare object; both are read.
The field and the arguments travel to the portal in the error extensions,
the way password policy violations already do, so the admin is told which
field to correct and what the limit is, in their own language and under
the label the form gives that field rather than its Keycloak name. The
message stays readable on its own for any other consumer, and a
constraint the portal does not recognise falls back to a generic wording
rather than surfacing a raw Keycloak key.
Editing a voter took the same path: it reported the whole Rust debug
rendering of the Keycloak failure as an internal error, so a refused
attribute read as a 500 with a HttpFailure { .. } dump in it.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Capping a field at the maximum its `length` validator allows discarded
what the admin typed past that point without saying anything, and then
saved the truncated value: typing "felix" into an attribute bounded at 2
characters stored "fe" and reported success. Silently storing a value the
admin did not enter is worse than refusing the save, and the cap could
not be corrected out of either, since a browser will not shorten a value
that is already over the limit, only refuse to extend it.
The bound is dropped for now and will come back in a form that states the
limit rather than enforcing it invisibly. Keycloak still enforces it on
save, and that refusal now names the field and the limit it broke, so
nothing goes unreported.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Keycloak refuses every attribute it objects to in one answer, as a list:
{"errors":[{"field":"ward","errorMessage":"error-invalid-value","params":["ward"]},
{"field":"roll","errorMessage":"error-invalid-length","params":["roll",1,2]}]}
Only the first was being reported, so an admin with several fields wrong
corrected them one save at a time, and in Keycloak's order rather than
the form's, which sends them up and down the form to find each one.
All of them are now reported together, up to ten. A profile whose import
mapped badly can have most of its attributes refused at once, which is
more than a message can usefully carry, so the rest are counted rather
than listed and the count states how many were left out. The extensions
carry the whole reported list, so the portal names and localizes each
field, and carry the total separately so it can say how many it did not
show.
The reporting is split from the parsing, so the cap and the wording are
exercised without standing up a Keycloak failure.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…form
Parent issue: sequentech/meta#12901
An attribute's `length` bounds were only discoverable by breaking them,
and only at the end of a save. They are now stated under the field, and
the value is checked when the field is left: breaking a bound marks the
field, says which bound it broke, and holds the save until it is
corrected.
Both bounds are checked the same way, rather than capping the field as it
is typed. A cap cannot express a minimum at all, and as a maximum it
discards the end of a pasted value without saying so and leaves a stored
value that already exceeds it impossible to correct, since a browser will
refuse to extend such a value but will not shorten it.
Only the `length` validator is read. The `inputTypeMaxlength` annotation
caps typing on the voter-facing forms but is not enforced on submit, so
stating it in the form would claim a rule the server does not apply.
Keycloak measures the trimmed value unless the attribute sets
`trim-disabled`, so the form measures the same string the server will,
and a minimum bounds only a value that is present: an absent one is a
matter for the attribute being required, which the form states
separately.
The wording is the wording Harvest already produces for the same
rejection, so a field reads identically whether it was caught in the form
or refused on save. A bound reached some other way is still refused on
save and reported as before.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actionsBot commented Aug 21, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-22 02:11 UTC

Parent issue: sequentech/meta#12901
A field whose `length` validator sets a maximum now stops accepting
characters once it is reached, so the maximum cannot be exceeded by
typing.
This is the cap that was removed earlier for truncating silently, brought
back on top of what has since been added rather than in place of it: the
bound is stated under the field before it is reached, and the value is
still checked when the field is left, so a value that is over the maximum
some other way is reported rather than passed over. That covers the case
the cap cannot: a stored value already longer than the maximum, which a
browser will refuse to extend but will not shorten, is now reported as
too long and holds the save until it is brought within the bound.
Pasting a longer value into a capped field still keeps only what fits
without warning. The maximum is stated under the field, so the limit is
at least visible, but the truncation itself is not announced.
The minimum is unaffected: it cannot be applied while typing, so it stays
a check made when the field is left.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitaiBot commented Aug 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 559146e1-74c3-4397-b033-e1b0b333044a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Findetonand others added 8 commits August 21, 2026 22:06
Parent issue: sequentech/meta#12901
A field that broke its bounds disabled the save without turning red, so
the save button went dead with nothing to point at. Only the basic
information fields were affected: react-admin's `sanitizeInputRestProps`
drops an `error` prop, so its inputs take their state solely from the
form's own validation, which these checks do not go through. They are now
coloured from the same state that produces the message, the way
EditPassword's password inputs already are.
A touched field also reports being required and left empty, not only
breaking a bound. An empty value breaks no bound, so it went unreported
while still blocking the save. The bounds are still reported too, which a
maximum should no longer reach now that the field stops accepting
characters at it, but a value can be over one for reasons the field does
not control.
The decision of what a field has to report is separated from its wording,
so it is exercised directly: required against optional, an empty value
against a bounded one, and an attribute that does not trim, where spaces
are a value rather than an absent one.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
An attribute marked `hidden` is kept off the voter-facing enrollment and
login forms by the login theme, but the Admin Portal had never read the
annotation, so hidden attributes appeared as voter columns, in the
columns selector and the filters, and as fields when creating or editing
a voter. Deployments that wanted them out of sight were hiding them with
per-attribute CSS in the tenant's own stylesheet, which has to name every
attribute and, for the columns selector, resort to hiding list items by
position.
The annotation is now read where the voter screens get their attribute
list, which covers the columns, the columns selector, the filters and
both forms at once, since all of them are handed that same list.
Hiding an attribute does not remove it: its value is still stored, still
carried through when a voter is edited, and still exported. The flag is
Sequent's own rather than Keycloak's, so it is read the way the
enrollment extension reads it, accepting the string Keycloak's admin
console writes as well as a boolean from a realm configuration, and it is
distinct from an `inputType` of `hidden`, which renders an attribute as a
hidden input rather than keeping it off the form.
The Approvals screens read the attribute list separately and are
unchanged.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
The save button was held only by `alwaysEnable`, which does not disable
anything: react-admin reads it as "stop forcing this button on" and falls
back to its own rule, which knows nothing about these errors and enables
the button whenever the form is dirty. The button looked held only
because `SimpleForm` is handed a record that is rebuilt on every
keystroke, so react-hook-form rarely considers the form dirty. Submitting
was still refused, but silently, from a button that looked live.
`disabled` is the prop that rule reads, so it is passed alongside.
Also moves the comment describing how a save failure is reported back
onto the function that does it, from the one that states a field's
bounds, where an earlier edit left it.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Stating every `length` bound put a hint under 23 of the 31 visible fields
of a real voter form, most of them for bounds no one reaches: Keycloak's
base attributes carry maxima in the hundreds as scaffolding, and clients
copy that for free-text attributes, so the one bound that mattered was
lost among "At most 255 characters" repeated down the form.
A maximum above a hundred is no longer stated, and neither is a minimum
of one, which says only that the value is present — something the field
already says by being required or not. Everything the validator sets is
still checked and still capped; this only decides what is worth saying up
front. On the form measured, the hints drop from 23 to 18, leaving the
bounds a client actually chose.
Date fields were already unaffected: they are entered through a picker
and are given no hint, no cap and no check.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Pressing Enter no longer discards the field being typed into. The
attribute inputs are uncontrolled and write back only on blur, and
submitting a form with Enter does not blur, so the value in the field
never reached the record and was never checked: an edit reported "no
changes to review" while the field on screen held the new value, and a
create saved the voter without it. Enter now commits the field instead of
submitting.
A Keycloak rejection that names no attribute is no longer reported as a
refused attribute. Every Keycloak error body has the same shape, and the
parse accepted any of them, so a rejected password on an admin user came
back as `"" has an invalid value` — less than the raw text it replaced.
Only an entry naming the attribute it refused is treated as one, on both
sides of the wire.
The select's options were declared `string[]` after an `Array.isArray`
narrowing, which admits `any[]` unchecked. A realm carrying numeric
options put numbers into the option label, the stored value and the
GraphQL payload, and rendered `1 - 1`. They are filtered to strings.
Also: guard the error walk against a non-array `graphQLErrors`, since it
runs inside the handler meant to report the failure; read a bound with
`Number` rather than `parseInt`, which took "12abc" for 12 and then
stated a rule the server does not enforce; refuse a `length` that is not
an object, whose `.length` would answer with a number; tolerate
`params: null`, which failed the parse for every attribute at once; keep
`authorize`'s status when it is not an authorization failure; and say how
many fields need correcting, since a disabled save button said nothing
when its cause had scrolled off screen. Basic information fields carry
`aria-invalid`, having conveyed their error by colour alone.
The stated bounds and the reported violation now quote the same numbers,
the hint having learned to leave some unsaid.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
The Enter handler was bound to the wrong element and did nothing. MUI's
TextField forwards onBlur, onChange and onFocus to the input and leaves
everything else on the FormControl wrapper, so the handler ran on a div
and blurring it was a no-op on an element that never had focus. Enter
neither committed nor submitted: it was simply swallowed. It is bound to
the input now, and commits the value where it stands rather than blurring
it, so the admin keeps their place in the form.
A refused attribute could still be reported without its name. The label
resolver answers with the empty string for an attribute the form does not
show — a hidden one, say — and the fallback that turned that back into
the attribute's own name had been removed as redundant. It resolves to
the name itself now, which is what the hidden-and-required case
documented alongside it depends on.
A violation could state a rule with the number missing. The message reads
using the bounds the field states, so the two cannot disagree over a
bound left unsaid, but it was interpolating from those too — and an
attribute bounded above a hundred has that maximum deliberately unstated,
so a stored value over it read "must be at most characters". The
wording follows the stated bounds; the numbers come from the bounds the
value was measured against.
The save button and the submit guard counted different things, one over
the attributes on the form and the other over every recorded error, so
the button could look ready while submitting silently did nothing. Both
read the same count now.
A constraint that is not mapped to a wording keeps Harvest's message,
which names the field and the constraint, rather than being flattened to
"has an invalid value". Keycloak's options validator reports
`error-invalid-value`, which is mapped, since the generic wording is
exactly right for it.
Also: the alert no longer counts the fields, which spared eight
languages a parenthesised plural for a number the highlighted fields
already show; the guard on a `length` that is not an object keeps its
narrowing but drops a rationale that described a hazard it does not
prevent; and the tutorial no longer promises that every bound is stated,
now that the ones nobody types into are not.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
A constraint with no wording of its own made the whole refusal fall back
to Harvest's message, so one unknown constraint cost every other refused
attribute its wording too — and Harvest's message is English, carries
Keycloak's raw constraint keys, states no bounds, and is capped at two
hundred characters, which cuts a batch off after about four attributes.
That is the mis-mapped import this was built to report, and reporting it
was the point of listing ten.
Each refused attribute is described on its own now: the ones whose
constraints have a wording keep it, bounds and all, and one that does not
names its attribute and says what Keycloak called it, rather than saying
only that something was invalid. Keycloak's options validator reports
`error-invalid-value`, which has a wording, since the generic one fits it
exactly.
Enter no longer clears a date. A half-entered date reads as empty and
Enter is the reflexive way to finish typing one, so committing that would
have wiped the stored date; clearing one is still done by emptying the
field and leaving it.
Committing on Enter does drop focus, which the comment claimed it did
not: the input is mounted under a key derived from its value, so writing
a changed value back replaces the input. It is the same place focus would
have gone by leaving the field, and the comment says so now.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Findeton
Findeton marked this pull request as ready for review August 22, 2026 01:36
Findetonand others added 3 commits August 22, 2026 01:43
…ates a Meet link
Parent issue: sequentech/meta#12901
Generating a Google Meet link reported a failure by pasting the HTTP
status and the entire response body into the page, so a failure read as
`Failed to generate Google Meet link. Status: 500. Body: {"message":...}`
— the handler's message buried in the JSON that carried it, at whatever
length it happened to be.
It now reads the reason the same way saving a voter does: the handler's
own message, first line, bounded. That was the last place reaching into
Hasura's error internals by hand, and it is what makes a shared helper
worth having rather than a second copy of one.
Its `catch (err: any)` is gone with it.
The strings here are still hardcoded English, unlike the rest of the
component's `googleMeet` block, which predates this and is left alone.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
The attribute tutorial covered how limits are configured, but nothing
described what an admin sees when one is broken: that fields are checked
as they are left, that a rejected save keeps the editor open with the
values as entered, and that the fields it concerns are named along with
the rule they broke.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parent issue: sequentech/meta#12901
Three things the section left an operator to find out by trying: that the
value is matched literally, so TRUE or a padded one hides nothing; that
the Approvals screens read the attribute list separately and still show
hidden attributes; and that hiding keeps a form readable rather than
restricting access, since the value still reaches the browser and the
exports. An attribute that must not be read is a matter for its
permissions.
Refs sequentech/meta#12901
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Findeton
Findeton merged commit cb53f31 into release/10.0Aug 22, 2026
30 of 31 checks passed
@Findeton
Findeton deleted the feat/meta-12901/release/10.0 branch August 22, 2026 02:07
Sign up for freeto 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.

1 participant

@Findeton