Surface backend 422 validation violations inline in the Subject editor#879
Merged
Conversation
Axios's default validateStatus throws on 4xx/5xx, which prevented RestSubjectRepository's existing response.ok checks from running for 422 responses. Set validateStatus to accept everything; the wrapper already constructs a Response with the correct status, and Response.ok naturally becomes false for 4xx/5xx.
SubjectViolation is the frontend mirror of the backend Violation wire shape. ValidationFailedError is the typed Error thrown by the REST persistence layer on HTTP 422 so the editor dialog can catch and surface the structured violations instead of the current generic toast. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Centralises the per-Value-Type empty-content notion. Used as part of the 422-surfacing work to reason about backend 'required' violations against current field state; broadly useful elsewhere.
The three write methods (createMainSubject, createChildSubject, updateSubject) now recognise HTTP 422, deserialise the body's violations array via a structural guard, and throw the typed error. Malformed 422 bodies log to console.error and fall through to the existing generic-error path so partial garbage never reaches the UI.
…olation Adds an optional serverViolations prop and clear-server-violation emit to every per-PropertyType input component. useStringValueInput merges server-sourced violations into the existing Codex error slot, keyed by valuePartIndex for multi-value inputs and falling back to the field-level summary for null index. Live errors take precedence when both are present on the same input; the clear emit fires only when the user actually edits a field that had a server violation, so the parent can drop it from the reactive set before the next save.
Adds a ValidationFailedError catch branch to handleSave that flows the backend's violations into reactive state, passed down via SubjectEditor to each per-PropertyType input. Anchorless violations (null propertyName or propertyName not in the schema) render in a form-level banner above the editor. Editing a field that had a violation emits up to the dialog so the matching entry is dropped from the reactive set, mirroring how live errors clear on edit. Two new i18n messages cover the toast and the schema-not-found banner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
004aa70 to
c02bbd0
Compare
Mirrors the pattern added to SubjectEditorDialog. The Creator dialog used its own catch block that fell to the generic-error toast for any throw, so the backend's structured 422 body was discarded and the user saw "Validation failed" with no field-level marking. Adds the same ValidationFailedError branch: reactive serverViolations state, flows down to SubjectEditor (which already supports the prop from Task 4), anchorless violations render in the form-level CdxMessage banner, the new neowiki-subject-editor-validation-failed toast key replaces the generic message. Editing a field that had a server violation emits clear-server-violation back up so the matching entry is dropped before the next save attempt.
c02bbd0 to
7834d39
Compare
A permanent live testbed for the backend's structured-validation responses and the editor's inline surfacing of them. Useful for reproducing each violation type through the real UI when working on the validation pipeline. Adds: - Schema:Validation Demo with text/url/select properties covering required, multi-value, and per-index scenarios. - Validation Clean — a Subject with every property filled in correctly, used as the starting point for each violation recipe. - Page Validation Demo — a hub page documenting enforcement setup, the recipes for inline-per-field and clear-on-edit, and the step-1-contract preservation check, with the backend wire shape and pointers to the relevant code paths.
The composable previously filtered each input value through the property type's validate() before adding it to the emitted StringValue. Invalid entries were dropped; only "valid" ones reached the model. Combined with backend diff-based enforcement, this had two confusing consequences: 1. Multi-value `valuePartIndex` violations from the backend were unreachable through the editor — e.g. changing an existing valid URL to `ftp://example.com/b` would just drop the entry on Save, so the diff against the prior state was empty and enforcement allowed the write. The user's intended edit silently vanished. 2. The frontend was acting as an authoritative validator. Backend enforcement existed but couldn't fire on data the frontend had already censored, undermining the whole 422-surfacing path. Emit every non-empty entry, including ones the live validator flags. `newStringValue` still trims and drops empties so mid-typing blanks don't pollute the model. Per-input live errors continue to render (unchanged). On Save, the backend validates the real proposed values and either accepts them or returns a structured 422 — which the editor now decorates inline next to the offending sub-input via the serverViolations machinery added in the previous commit. Updates the two regression specs that pinned the old censor-on-input behaviour to reflect the new contract.
Member
Author
Recording.2026-06-08.182359.mp4 |
2 tasks
alistair3149
added a commit
that referenced
this pull request
Jun 10, 2026
#883) * Extract shared field-level server-violation handling into a composable Follows-up to #879. The four single-value inputs (Boolean, Number, Date, DateTime) each carried a byte-identical copy of the server-violation fallback (show the field-level violation when there is no live error) and the clear-on-edit emit. This extracts that into `useFieldServerViolation`, mirroring how the multi-value inputs already share `useStringValueInput`. Net -49 lines across the four components; the behavior is unchanged. Also adds the missing `neowiki-field-type-mismatch` message. The frontend formats every violation code as `neowiki-field-<code>`, but `type-mismatch` had no key, so a `type-mismatch` violation surfaced (e.g. a pre-existing one riding along in an enforced-write 422 after a Schema type change) rendered as the raw `⧼neowiki-field-type-mismatch⧽` placeholder. `label-required` is deliberately left without a key: it is unreachable on every current frontend path (write actions reject an empty label with a 400 before validation runs, and nothing yet renders the validate-endpoint output), so adding it now would be speculative. The new composable is unit-tested directly (the four components had no tests for this logic before) and exported from public-api.ts for consistency with the other composables. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Align useFieldServerViolation with the Ref + emit composable pattern and cover the wiring Review follow-up on the previous commit: - The composable now takes Ref params and the component's emit function (reusing ValueInputEmitFunction), matching the useStringValueInput precedent. This removes the repeated clear-emit wrapper lambda at all four call sites and the inline re-declaration of the payload type. - Corrected the docblock rationale for Select/Relation: they do field-level handling, not per-index handling. - Composable spec: added reactivity transition tests (live error resolving back to the server violation; parent removing the violation) and a clear test for a violation on a different property. - Component specs: added Server violations blocks covering the wiring through the real composable - rendering into CdxField, the clear-on-edit emit, and (Date/DateTime) the display formatter being passed. Verified to fail under wiring mutations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: alistair3149 <alistair31494322@gmail.com>
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 free
to 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.
Follows-up to #856.
Summary
When
$wgNeoWikiEnforceValidationis on and a save is blocked, the backend returns a 422 with a structuredviolations[]array. Today the editor discards it and shows the opaque toast "Request failed with status code 422". After this PR, the editor parses the body, marks the offending fields inline, keeps the dialog open, and shows a toast that names the subject and points at the highlighted fields. Editing a flagged field clears its red border immediately. The Subject create dialog gets the same treatment. With enforcement off, behaviour is unchanged — writes always persist and violations come back as advisory data in the 200 response.What changed
Persistence.
ProductionHttpClientwidensvalidateStatusto accept 422 alongside 2xx so the response reachesRestSubjectRepositoryinstead of axios throwing first. Other non-2xx still reject, preservingCsrfSendingHttpClient's 403 refresh-and-retry. The three write methods (createMainSubject,createChildSubject,updateSubject) parse the 422 body via a structural guard and throw a typedValidationFailedError; malformed bodies log toconsole.errorand fall through to the existing generic-error path.Domain. New
SubjectViolationinterface mirrors the backend wire shape one-to-one. NewValidationFailedError extends Errorcarriesreadonly violations. IndependentisValueEmpty(value: Value | undefined)helper centralises the per-Value-Type empty notion (one of the wins from #807 worth folding in early).Editor inputs.
ValueInputContractgains an optionalserverViolationsprop and aclear-server-violationemit.useStringValueInputmerges server-sourced violations into the Codex error slot keyed byvaluePartIndex(live errors take precedence on the same input), and surfaces field-level (valuePartIndex: null) server violations through the field summary regardless of multi/single so they can't silently vanish. Every per-PropertyType input — Text, Url, Number, Boolean, Date, DateTime, Select, Relation — passes the new prop/emit through. The composable also stops filtering invalid values out ofupdate:modelValue: the backend is now the authoritative validator, so a bad URL the user types is sent on save and rejected with a real per-index violation rather than silently dropped.Dialogs. Both
SubjectEditorDialog::handleSaveandSubjectCreatorDialog::handleSavecatchValidationFailedError, set reactiveserverViolationsstate, fire the newneowiki-subject-editor-validation-failedtoast, and leave the dialog open. Violations whosepropertyNamedoesn't match a rendered statement (or that havepropertyName: null, e.g.schema-not-found) render in a form-levelCdxMessagebanner above the editor. Editing a field that had a server violation emits up to drop the matching entry so the red border clears before the next save.Demo data. New
Schema:Validation Demo,Validation Cleansubject, andValidation Demohub page — a permanent live testbed for the 422-surfacing recipes. Useful for QA, future feature work, and onboarding.Out of scope
ValueValidationError(live per-input errors) withSubjectViolation. They share the Codex slot but stay as distinct types here. Worth revisiting once enforcement has been in use for a while.ValueValidationError. Backend doesn't emit severity; nothing to surface yet.SubjectValidatorfrontend class. Registered as a service but never called — the spec deliberately leaves it alone. Delete vs. repurpose decided separately.Known surface gaps
SelectInputandRelationInputmulti-mode collapsevaluePartIndex-keyed server violations into a single field-level message rather than per-chip decoration. Codex's multiselect and lookup widgets don't expose a per-chip error slot, and chip positions aren't stable. Showing only the first relevant violation is the pragmatic landing point.Manual Browser Check
Set
$wgNeoWikiEnforceValidation = trueinLocalSettings.php. Then, on the imported demo:Validation Cleansubject page → action=subjects → Edit the main subject. Backspace the Title value and click Save changes. Confirm: dialog stays open, Title gets a red border with "Please provide a value.", toast reads "Validation failed for 'Validation Clean'. Please fix the highlighted fields." Network panel shows the PUT returning 422.https://example.com/btoftp://example.com/band click Save changes. Confirm: only the second sub-input gets the red border (the first stays clean), the 422 response body carriesvaluePartIndex: 1, the bad URL stays in the input so the user can fix it.$wgNeoWikiEnforceValidation = false. Repeat step 1's edit. Confirm: PUT returns 200, dialog closes, success toast — proves the step-1 contract from Surface violations on Subject write endpoints #855 is preserved.The console should be clean throughout, except for the browser's own "Failed to load resource: ... 422" line which fires for any 422 response.
The
Validation Demohub page documents these recipes in-wiki.Test plan
make tscigreen (build + full vitest suite at 953/953 + lint).make phpunitgreen — backend untouched; the new demo subject round-trips throughSubjectContentDataSerializerTest.🤖 Generated with Claude Code