ec27228a8e
Em dashes (—) and en dashes (–) had spread across comments, docs, tests, and a few UI strings, reading as AI-generated boilerplate rather than house style. Replaced each with punctuation matching its context: colon for explanatory clauses, comma for asides, plain hyphen for numeric/legal ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for paired-dash asides. messages/en.json and messages/sv.json were fixed by hand together to keep sv/en in sync. Left untouched where the dash is the functional subject rather than decorative punctuation: date-range-parser.ts's separator regex, charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the agent system-prompt files that already instruct against em dashes, and a golden iXBRL test fixture compared byte-for-byte. Also fixes two bugs surfaced along the way: an off-by-one in ApiKeysPanel's scope-label split (a leftover from an earlier partial pass), and a charset-repair test that had lost the literal en-dash it exists to verify. Regenerated the agent atom seed migration (skills:generate) since 27 SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes, with an explicit carve-out for the functional-dash cases above. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
3.4 KiB
TypeScript
115 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Client-side helper that turns a fetch failure into a Swedish toast with
|
|
* remediation hint and the X-Request-Id for support reference.
|
|
*
|
|
* Accepts:
|
|
* - the `{ error: {...} }` envelope produced by the route wrapper
|
|
* - a Response object (the helper reads it for you)
|
|
* - a raw Error / string (falls back to getErrorMessage)
|
|
*
|
|
* Usage:
|
|
* const showError = useErrorToast()
|
|
* const res = await fetch('/api/invoices/123/send', { method: 'POST' })
|
|
* if (!res.ok) {
|
|
* await showError(res, { context: 'invoice' })
|
|
* return
|
|
* }
|
|
*/
|
|
|
|
import { useToast } from '@/components/ui/use-toast'
|
|
import { getErrorMessage } from '@/lib/errors/get-error-message'
|
|
import type { ErrorEnvelope } from '@/lib/errors/get-structured-error'
|
|
|
|
type ErrorContext =
|
|
| 'invoice'
|
|
| 'supplier_invoice'
|
|
| 'customer'
|
|
| 'supplier'
|
|
| 'transaction'
|
|
| 'journal_entry'
|
|
| 'settings'
|
|
| 'auth'
|
|
| 'salary'
|
|
|
|
interface ShowErrorOptions {
|
|
context?: ErrorContext
|
|
/** Override the toast title (Swedish summary). Defaults to envelope.message. */
|
|
title?: string
|
|
}
|
|
|
|
interface NormalizedError {
|
|
message: string
|
|
remediation?: string
|
|
requestId?: string
|
|
code?: string
|
|
}
|
|
|
|
async function normalize(input: unknown): Promise<NormalizedError> {
|
|
// Response: try to read JSON body and X-Request-Id header
|
|
if (input instanceof Response) {
|
|
const requestId = input.headers.get('X-Request-Id') ?? undefined
|
|
let body: unknown = null
|
|
try {
|
|
body = await input.json()
|
|
} catch {
|
|
// ignore: body might be empty
|
|
}
|
|
const fromBody = readEnvelope(body)
|
|
return {
|
|
message: fromBody.message ?? getErrorMessage(body, { statusCode: input.status }),
|
|
remediation: fromBody.remediation,
|
|
requestId: fromBody.requestId ?? requestId,
|
|
code: fromBody.code,
|
|
}
|
|
}
|
|
|
|
const fromBody = readEnvelope(input)
|
|
if (fromBody.message) {
|
|
return fromBody
|
|
}
|
|
return { message: getErrorMessage(input) }
|
|
}
|
|
|
|
function readEnvelope(input: unknown): NormalizedError {
|
|
if (!input || typeof input !== 'object') return { message: '' }
|
|
const obj = input as Record<string, unknown>
|
|
const errObj = obj.error
|
|
if (errObj && typeof errObj === 'object') {
|
|
const e = errObj as Partial<ErrorEnvelope['error']>
|
|
return {
|
|
message: typeof e.message === 'string' ? e.message : '',
|
|
remediation:
|
|
e.remediation && typeof e.remediation === 'object'
|
|
? (e.remediation as { description?: string }).description
|
|
: undefined,
|
|
requestId: typeof e.requestId === 'string' ? e.requestId : undefined,
|
|
code: typeof e.code === 'string' ? e.code : undefined,
|
|
}
|
|
}
|
|
return { message: '' }
|
|
}
|
|
|
|
export function useErrorToast() {
|
|
const { toast } = useToast()
|
|
|
|
return async function showError(input: unknown, options: ShowErrorOptions = {}) {
|
|
const norm = await normalize(input)
|
|
const title = options.title ?? norm.message ?? getErrorMessage(input, { context: options.context })
|
|
|
|
const descriptionParts: string[] = []
|
|
if (norm.remediation) descriptionParts.push(norm.remediation)
|
|
if (norm.requestId) descriptionParts.push(`Felreferens: ${norm.requestId}`)
|
|
if (process.env.NODE_ENV !== 'production' && norm.code) {
|
|
descriptionParts.push(`Kod: ${norm.code}`)
|
|
}
|
|
|
|
toast({
|
|
variant: 'destructive',
|
|
title,
|
|
description: descriptionParts.length > 0 ? descriptionParts.join(' · ') : undefined,
|
|
})
|
|
}
|
|
}
|