From 55a72bfe12659f3c9dac6565315ed5697e6d0276 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 07:55:59 +0200 Subject: [PATCH 1/3] POLISH-1: gridify emits canonical '='; localize litter InvalidParentGender 400 (ApiError body) + de strings --- gerbil-manager-web/src/api/client.ts | 23 +++++++++++++++++-- gerbil-manager-web/src/api/gridify.ts | 4 +++- gerbil-manager-web/src/pages/WurfFormPage.tsx | 17 +++++++++++--- gerbil-manager-web/src/strings/de.ts | 3 +++ 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/gerbil-manager-web/src/api/client.ts b/gerbil-manager-web/src/api/client.ts index 90498e0..dbddb10 100644 --- a/gerbil-manager-web/src/api/client.ts +++ b/gerbil-manager-web/src/api/client.ts @@ -13,15 +13,27 @@ export class ApiError extends Error { readonly status: number | null /** The request path that failed (for diagnostics/logging). */ readonly path?: string + /** Parsed error response body, if any (e.g. { code, ... } for 400s). */ + readonly body?: unknown - constructor(message: string, status: number | null, path?: string) { + constructor(message: string, status: number | null, path?: string, body?: unknown) { super(message) this.name = 'ApiError' this.status = status this.path = path + this.body = body } } +/** Read a `code` field off a parsed ApiError body, if present. */ +export function errorCode(err: unknown): string | null { + if (err instanceof ApiError && err.body && typeof err.body === 'object' && 'code' in err.body) { + const code = (err.body as { code: unknown }).code + return typeof code === 'string' ? code : null + } + return null +} + function errorMessageFor(status: number): string { if (status === 404) return de.api.errors.notFound if (status >= 500) return de.api.errors.server @@ -40,7 +52,14 @@ async function request(path: string, init?: RequestInit): Promise { } if (!response.ok) { - throw new ApiError(errorMessageFor(response.status), response.status, path) + // Best-effort parse of a JSON error body (e.g. { code: "InvalidParentGender" }). + let body: unknown + try { + body = await response.clone().json() + } catch { + body = undefined + } + throw new ApiError(errorMessageFor(response.status), response.status, path, body) } if (response.status === 204) { diff --git a/gerbil-manager-web/src/api/gridify.ts b/gerbil-manager-web/src/api/gridify.ts index 19bf8dd..cce4f66 100644 --- a/gerbil-manager-web/src/api/gridify.ts +++ b/gerbil-manager-web/src/api/gridify.ts @@ -47,8 +47,10 @@ export function condition(c: FilterCondition): string { const suffix = c.caseInsensitive === false ? '' : '/i' return `${c.field}=*${escaped}*${suffix}` } + // Gridify's equals operator is a single '='; callers use '==' semantically. + const op = c.op === '==' ? '=' : c.op const suffix = c.caseInsensitive ? '/i' : '' - return `${c.field}${c.op}${escaped}${suffix}` + return `${c.field}${op}${escaped}${suffix}` } /** Join conditions with AND (comma). Falsy/empty conditions are dropped. */ diff --git a/gerbil-manager-web/src/pages/WurfFormPage.tsx b/gerbil-manager-web/src/pages/WurfFormPage.tsx index 6ba21ba..17d402d 100644 --- a/gerbil-manager-web/src/pages/WurfFormPage.tsx +++ b/gerbil-manager-web/src/pages/WurfFormPage.tsx @@ -1,6 +1,7 @@ import { useMemo, useState, type FormEvent } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' import { de } from '../strings/de' +import { errorCode } from '../api/client' import { createLitter, getLitter, updateLitter } from '../api/litters' import { listGerbils } from '../api/gerbils' import type { CreateLitter } from '../api/types' @@ -49,6 +50,7 @@ export default function WurfFormPage() { const [form, setForm] = useState(EMPTY) const [errors, setErrors] = useState>>({}) + const [submitError, setSubmitError] = useState(null) const [goHomeTouched, setGoHomeTouched] = useState(false) const [initializedFor, setInitializedFor] = useState(null) @@ -116,9 +118,18 @@ export default function WurfFormPage() { expectedGoHomeDate: nn(form.expectedGoHomeDate), notes: nn(form.notes), } + setSubmitError(null) const result = await mutation.run(body) - if (result.ok) navigate(`/wuerfe/${result.value.id}`) - // On failure mutation.error drives the inline alert; run() never throws. + if (result.ok) { + navigate(`/wuerfe/${result.value.id}`) + return + } + // Localize the backend's parent-gender 400 by its code; else generic message. + setSubmitError( + errorCode(result.cause) === 'InvalidParentGender' + ? t.validation.invalidParentGender + : result.error, + ) } if (isEdit && existing.loading) return

{de.common.loading}

@@ -223,7 +234,7 @@ export default function WurfFormPage() {