Uh oh!
There was an error while loading. Please reload this page.
✨ Admin Portal: Voter editing quality of life features - #3078
Merged
Conversation
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>
|
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>
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
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>
Parent issue: sequentech/meta#12901 Refs sequentech/meta#12901 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findeton
marked this pull request as ready for review
August 22, 2026 01:36
…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>
Uh oh!
There was an error while loading. Please reload this page.
Findeton added a commit
that referenced
this pull request
Aug 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parent issue: https://github.com/sequentech/meta/issues/12901