POLISH-1: gridify emits canonical '='; localize litter InvalidParentGender 400 (ApiError body) + de strings
This commit is contained in:
@@ -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<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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<FormState>(EMPTY)
|
||||
const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({})
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [goHomeTouched, setGoHomeTouched] = useState(false)
|
||||
const [initializedFor, setInitializedFor] = useState<string | null>(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 <p className="muted">{de.common.loading}</p>
|
||||
@@ -223,7 +234,7 @@ export default function WurfFormPage() {
|
||||
<textarea value={form.notes} onChange={(e) => set('notes', e.target.value)} />
|
||||
</label>
|
||||
|
||||
{mutation.error && <div className="alert alert--error">{mutation.error}</div>}
|
||||
{submitError && <div className="alert alert--error">{submitError}</div>}
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="submit" className="btn btn--primary" disabled={mutation.pending}>
|
||||
|
||||
@@ -97,6 +97,7 @@ export const de = {
|
||||
},
|
||||
tabPlaceholder: 'Dieser Bereich entsteht in einem späteren Schritt.',
|
||||
notFound: 'Diese Rennmaus wurde nicht gefunden.',
|
||||
moreData: 'Weitere Daten',
|
||||
},
|
||||
// Formular (anlegen/bearbeiten)
|
||||
form: {
|
||||
@@ -184,6 +185,7 @@ export const de = {
|
||||
validation: {
|
||||
nameRequired: 'Bitte eine Bezeichnung eingeben.',
|
||||
dateRequired: 'Bitte ein Wurfdatum angeben.',
|
||||
invalidParentGender: 'Der Vater muss männlich und die Mutter weiblich sein.',
|
||||
},
|
||||
// Zuchtpaar-Übersicht
|
||||
pairs: {
|
||||
@@ -610,6 +612,7 @@ export const de = {
|
||||
no: 'Nein',
|
||||
delete: 'Löschen',
|
||||
confirmDelete: 'Wirklich löschen?',
|
||||
pageNav: 'Seitennavigation',
|
||||
},
|
||||
// Genetik-Warnungen: der Engine (src/genetics) liefert nur CODES,
|
||||
// die deutschen Texte stehen hier. Schlüssel = GeneticsWarningCode-Werte.
|
||||
|
||||
Reference in New Issue
Block a user