Commit 24bf9acc1330cecdcb366b7b8fa15202458addc5
1 parent
7a3ebe64
fix(web): DynamicExtFields on edit pages + user-friendly error messages
Showing
3 changed files
with
20 additions
and
4 deletions
web/src/api/client.ts
| @@ -110,8 +110,12 @@ export async function apiFetch<T>( | @@ -110,8 +110,12 @@ export async function apiFetch<T>( | ||
| 110 | if (text) { | 110 | if (text) { |
| 111 | try { | 111 | try { |
| 112 | body = JSON.parse(text) | 112 | body = JSON.parse(text) |
| 113 | - const m = (body as { message?: unknown }).message | ||
| 114 | - if (typeof m === 'string') message = m | 113 | + // Spring ProblemDetail uses "detail"; fallback to "message" |
| 114 | + const parsed = body as { detail?: unknown; message?: unknown } | ||
| 115 | + const d = parsed.detail | ||
| 116 | + const m = parsed.message | ||
| 117 | + if (typeof d === 'string') message = d | ||
| 118 | + else if (typeof m === 'string') message = m | ||
| 115 | } catch { | 119 | } catch { |
| 116 | body = text | 120 | body = text |
| 117 | message = text | 121 | message = text |
| @@ -171,7 +175,8 @@ export const catalog = { | @@ -171,7 +175,8 @@ export const catalog = { | ||
| 171 | itemType: string; baseUomCode: string; active?: boolean | 175 | itemType: string; baseUomCode: string; active?: boolean |
| 172 | }) => apiFetch<Item>('/api/v1/catalog/items', { method: 'POST', body: JSON.stringify(body) }), | 176 | }) => apiFetch<Item>('/api/v1/catalog/items', { method: 'POST', body: JSON.stringify(body) }), |
| 173 | updateItem: (id: string, body: { | 177 | updateItem: (id: string, body: { |
| 174 | - name?: string; description?: string | null; itemType?: string; active?: boolean | 178 | + name?: string; description?: string | null; itemType?: string; active?: boolean; |
| 179 | + ext?: Record<string, unknown> | ||
| 175 | }) => apiFetch<Item>(`/api/v1/catalog/items/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), | 180 | }) => apiFetch<Item>(`/api/v1/catalog/items/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), |
| 176 | listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'), | 181 | listUoms: () => apiFetch<Uom[]>('/api/v1/catalog/uoms'), |
| 177 | } | 182 | } |
| @@ -188,7 +193,8 @@ export const partners = { | @@ -188,7 +193,8 @@ export const partners = { | ||
| 188 | }) => apiFetch<Partner>('/api/v1/partners/partners', { method: 'POST', body: JSON.stringify(body) }), | 193 | }) => apiFetch<Partner>('/api/v1/partners/partners', { method: 'POST', body: JSON.stringify(body) }), |
| 189 | update: (id: string, body: { | 194 | update: (id: string, body: { |
| 190 | name?: string; type?: string; | 195 | name?: string; type?: string; |
| 191 | - email?: string | null; phone?: string | null | 196 | + email?: string | null; phone?: string | null; |
| 197 | + ext?: Record<string, unknown> | ||
| 192 | }) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), | 198 | }) => apiFetch<Partner>(`/api/v1/partners/partners/${id}`, { method: 'PATCH', body: JSON.stringify(body) }), |
| 193 | } | 199 | } |
| 194 | 200 |
web/src/pages/EditItemPage.tsx
| @@ -5,6 +5,7 @@ import type { Item } from '@/types/api' | @@ -5,6 +5,7 @@ import type { Item } from '@/types/api' | ||
| 5 | import { PageHeader } from '@/components/PageHeader' | 5 | import { PageHeader } from '@/components/PageHeader' |
| 6 | import { Loading } from '@/components/Loading' | 6 | import { Loading } from '@/components/Loading' |
| 7 | import { ErrorBox } from '@/components/ErrorBox' | 7 | import { ErrorBox } from '@/components/ErrorBox' |
| 8 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | ||
| 8 | 9 | ||
| 9 | const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const | 10 | const ITEM_TYPES = ['GOOD', 'SERVICE', 'DIGITAL'] as const |
| 10 | 11 | ||
| @@ -17,6 +18,7 @@ export function EditItemPage() { | @@ -17,6 +18,7 @@ export function EditItemPage() { | ||
| 17 | const [itemType, setItemType] = useState<string>('GOOD') | 18 | const [itemType, setItemType] = useState<string>('GOOD') |
| 18 | const [active, setActive] = useState(true) | 19 | const [active, setActive] = useState(true) |
| 19 | const [loading, setLoading] = useState(true) | 20 | const [loading, setLoading] = useState(true) |
| 21 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | ||
| 20 | const [submitting, setSubmitting] = useState(false) | 22 | const [submitting, setSubmitting] = useState(false) |
| 21 | const [error, setError] = useState<Error | null>(null) | 23 | const [error, setError] = useState<Error | null>(null) |
| 22 | 24 | ||
| @@ -28,6 +30,7 @@ export function EditItemPage() { | @@ -28,6 +30,7 @@ export function EditItemPage() { | ||
| 28 | setDescription(i.description ?? '') | 30 | setDescription(i.description ?? '') |
| 29 | setItemType(i.itemType) | 31 | setItemType(i.itemType) |
| 30 | setActive(i.active) | 32 | setActive(i.active) |
| 33 | + setExt(i.ext || {}) | ||
| 31 | }) | 34 | }) |
| 32 | .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | 35 | .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) |
| 33 | .finally(() => setLoading(false)) | 36 | .finally(() => setLoading(false)) |
| @@ -41,6 +44,7 @@ export function EditItemPage() { | @@ -41,6 +44,7 @@ export function EditItemPage() { | ||
| 41 | await catalog.updateItem(id, { | 44 | await catalog.updateItem(id, { |
| 42 | name, itemType, active, | 45 | name, itemType, active, |
| 43 | description: description || null, | 46 | description: description || null, |
| 47 | + ...(Object.keys(ext).length > 0 ? { ext } : {}), | ||
| 44 | }) | 48 | }) |
| 45 | navigate('/items') | 49 | navigate('/items') |
| 46 | } catch (err: unknown) { | 50 | } catch (err: unknown) { |
| @@ -85,6 +89,7 @@ export function EditItemPage() { | @@ -85,6 +89,7 @@ export function EditItemPage() { | ||
| 85 | className="rounded border-slate-300" id="active" /> | 89 | className="rounded border-slate-300" id="active" /> |
| 86 | <label htmlFor="active" className="text-sm text-slate-700">Active</label> | 90 | <label htmlFor="active" className="text-sm text-slate-700">Active</label> |
| 87 | </div> | 91 | </div> |
| 92 | + <DynamicExtFields entityName="Item" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} /> | ||
| 88 | {error && <ErrorBox error={error} />} | 93 | {error && <ErrorBox error={error} />} |
| 89 | <button type="submit" className="btn-primary" disabled={submitting}> | 94 | <button type="submit" className="btn-primary" disabled={submitting}> |
| 90 | {submitting ? 'Saving…' : 'Save Changes'} | 95 | {submitting ? 'Saving…' : 'Save Changes'} |
web/src/pages/EditPartnerPage.tsx
| @@ -5,6 +5,7 @@ import type { Partner } from '@/types/api' | @@ -5,6 +5,7 @@ import type { Partner } from '@/types/api' | ||
| 5 | import { PageHeader } from '@/components/PageHeader' | 5 | import { PageHeader } from '@/components/PageHeader' |
| 6 | import { Loading } from '@/components/Loading' | 6 | import { Loading } from '@/components/Loading' |
| 7 | import { ErrorBox } from '@/components/ErrorBox' | 7 | import { ErrorBox } from '@/components/ErrorBox' |
| 8 | +import { DynamicExtFields } from '@/components/DynamicExtFields' | ||
| 8 | 9 | ||
| 9 | const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const | 10 | const PARTNER_TYPES = ['CUSTOMER', 'SUPPLIER', 'BOTH'] as const |
| 10 | 11 | ||
| @@ -17,6 +18,7 @@ export function EditPartnerPage() { | @@ -17,6 +18,7 @@ export function EditPartnerPage() { | ||
| 17 | const [email, setEmail] = useState('') | 18 | const [email, setEmail] = useState('') |
| 18 | const [phone, setPhone] = useState('') | 19 | const [phone, setPhone] = useState('') |
| 19 | const [loading, setLoading] = useState(true) | 20 | const [loading, setLoading] = useState(true) |
| 21 | + const [ext, setExt] = useState<Record<string, unknown>>({}) | ||
| 20 | const [submitting, setSubmitting] = useState(false) | 22 | const [submitting, setSubmitting] = useState(false) |
| 21 | const [error, setError] = useState<Error | null>(null) | 23 | const [error, setError] = useState<Error | null>(null) |
| 22 | 24 | ||
| @@ -28,6 +30,7 @@ export function EditPartnerPage() { | @@ -28,6 +30,7 @@ export function EditPartnerPage() { | ||
| 28 | setType(p.type) | 30 | setType(p.type) |
| 29 | setEmail(p.email ?? '') | 31 | setEmail(p.email ?? '') |
| 30 | setPhone(p.phone ?? '') | 32 | setPhone(p.phone ?? '') |
| 33 | + setExt(p.ext || {}) | ||
| 31 | }) | 34 | }) |
| 32 | .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) | 35 | .catch((e: unknown) => setError(e instanceof Error ? e : new Error(String(e)))) |
| 33 | .finally(() => setLoading(false)) | 36 | .finally(() => setLoading(false)) |
| @@ -42,6 +45,7 @@ export function EditPartnerPage() { | @@ -42,6 +45,7 @@ export function EditPartnerPage() { | ||
| 42 | name, type, | 45 | name, type, |
| 43 | email: email || null, | 46 | email: email || null, |
| 44 | phone: phone || null, | 47 | phone: phone || null, |
| 48 | + ...(Object.keys(ext).length > 0 ? { ext } : {}), | ||
| 45 | }) | 49 | }) |
| 46 | navigate('/partners') | 50 | navigate('/partners') |
| 47 | } catch (err: unknown) { | 51 | } catch (err: unknown) { |
| @@ -86,6 +90,7 @@ export function EditPartnerPage() { | @@ -86,6 +90,7 @@ export function EditPartnerPage() { | ||
| 86 | className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> | 90 | className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> |
| 87 | </div> | 91 | </div> |
| 88 | </div> | 92 | </div> |
| 93 | + <DynamicExtFields entityName="Partner" values={ext} onChange={(k, v) => setExt(prev => ({ ...prev, [k]: v }))} /> | ||
| 89 | {error && <ErrorBox error={error} />} | 94 | {error && <ErrorBox error={error} />} |
| 90 | <button type="submit" className="btn-primary" disabled={submitting}> | 95 | <button type="submit" className="btn-primary" disabled={submitting}> |
| 91 | {submitting ? 'Saving…' : 'Save Changes'} | 96 | {submitting ? 'Saving…' : 'Save Changes'} |