Skip to content

Surface backend 422 validation violations inline in the Subject editor#879

Merged
JeroenDeDauw merged 9 commits into
masterfrom
frontend-422-surfacing
Jun 10, 2026
Merged

Surface backend 422 validation violations inline in the Subject editor#879
JeroenDeDauw merged 9 commits into
masterfrom
frontend-422-surfacing

Conversation

@alistair3149

@alistair3149 alistair3149 commented Jun 8, 2026

Copy link
Copy Markdown
Member

Follows-up to #856.

Summary

When $wgNeoWikiEnforceValidation is on and a save is blocked, the backend returns a 422 with a structured violations[] 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. ProductionHttpClient widens validateStatus to accept 422 alongside 2xx so the response reaches RestSubjectRepository instead of axios throwing first. Other non-2xx still reject, preserving CsrfSendingHttpClient's 403 refresh-and-retry. The three write methods (createMainSubject, createChildSubject, updateSubject) parse the 422 body via a structural guard and throw a typed ValidationFailedError; malformed bodies log to console.error and fall through to the existing generic-error path.

Domain. New SubjectViolation interface mirrors the backend wire shape one-to-one. New ValidationFailedError extends Error carries readonly violations. Independent isValueEmpty(value: Value | undefined) helper centralises the per-Value-Type empty notion (one of the wins from #807 worth folding in early).

Editor inputs. ValueInputContract gains an optional serverViolations prop and a clear-server-violation emit. useStringValueInput merges server-sourced violations into the Codex error slot keyed by valuePartIndex (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 of update: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::handleSave and SubjectCreatorDialog::handleSave catch ValidationFailedError, set reactive serverViolations state, fire the new neowiki-subject-editor-validation-failed toast, and leave the dialog open. Violations whose propertyName doesn't match a rendered statement (or that have propertyName: null, e.g. schema-not-found) render in a form-level CdxMessage banner 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 Clean subject, and Validation Demo hub page — a permanent live testbed for the 422-surfacing recipes. Useful for QA, future feature work, and onboarding.

Out of scope

  • UX polish. This PR wires up the surfacing; refinements to look-and-feel are deliberately deferred to a separate follow-up to keep the scope tight.
  • Approach B from the spec — unifying ValueValidationError (live per-input errors) with SubjectViolation. They share the Codex slot but stay as distinct types here. Worth revisiting once enforcement has been in use for a while.
  • Severity field on ValueValidationError. Backend doesn't emit severity; nothing to surface yet.
  • Aggregate SubjectValidator frontend class. Registered as a service but never called — the spec deliberately leaves it alone. Delete vs. repurpose decided separately.
  • Other Refactor frontend validation: declarative constraints + interpreter #807 wins — RelationType regression tests and the DateTime empty-StringValue fix — are independent small follow-ups.

Known surface gaps

  • SelectInput and RelationInput multi-mode collapse valuePartIndex-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 = true in LocalSettings.php. Then, on the imported demo:

  1. Open the Validation Clean subject 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.
  2. With the red border still showing, type any character into Title. Confirm the red border clears immediately, before re-clicking Save.
  3. Click Save changes again. Confirm: success toast, dialog closes.
  4. Reopen the editor. Change the second Tags entry from https://example.com/b to ftp://example.com/b and click Save changes. Confirm: only the second sub-input gets the red border (the first stays clean), the 422 response body carries valuePartIndex: 1, the bad URL stays in the input so the user can fix it.
  5. Click Add subject under Other Subjects on this Page, pick Validation Demo, leave Title empty, click Create subject. Confirm: 422, dialog stays open, Title decorated, validation-failed toast.
  6. Set $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 Demo hub page documents these recipes in-wiki.

Test plan

  • make tsci green (build + full vitest suite at 953/953 + lint).
  • Plan-required narrow filters green: Value 227, RestSubjectRepository 20, SubjectEditorDialog 19, SubjectCreatorDialog 53.
  • make phpunit green — backend untouched; the new demo subject round-trips through SubjectContentDataSerializerTest.
  • Reviewer: walk the Manual Browser Check above.
  • Reviewer: confirm no console errors during the Create-side 422 step.

🤖 Generated with Claude Code

alistair3149 and others added 6 commits June 8, 2026 18:15
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>
@alistair3149 alistair3149 force-pushed the frontend-422-surfacing branch from 004aa70 to c02bbd0 Compare June 8, 2026 22:27
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.
@alistair3149 alistair3149 force-pushed the frontend-422-surfacing branch from c02bbd0 to 7834d39 Compare June 8, 2026 22:35
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.
@alistair3149 alistair3149 requested a review from JeroenDeDauw June 8, 2026 22:42
@alistair3149

Copy link
Copy Markdown
Member Author
Recording.2026-06-08.182359.mp4

@alistair3149 alistair3149 marked this pull request as ready for review June 8, 2026 22:45
@JeroenDeDauw JeroenDeDauw merged commit 269b2c1 into master Jun 10, 2026
12 checks passed
@JeroenDeDauw JeroenDeDauw deleted the frontend-422-surfacing branch June 10, 2026 15:04
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>
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