fix(api): treat empty strings as null in optional fields; show issues on web
- zod-utils.ts: optionalRegex/optionalEmail/optionalText with preprocess('' -> null)
- clients/organizations/services schemas now accept empty strings for optional fields
(controlled inputs naturally produce '' instead of null)
- ApiError.prettyMessage() unfolds zod fieldErrors so users see which field failed
Reproduce: POST /api/clients with email="" or empty inn returned 400 validation_error.
Now it succeeds (empty stays null), and any real validation error names the field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,21 @@ export class ApiError extends Error {
|
||||
constructor(public status: number, public code: string, public details?: unknown) {
|
||||
super(`${status} ${code}`);
|
||||
}
|
||||
|
||||
/** Удобочитаемое сообщение для UI: разворачивает zod issues если есть. */
|
||||
prettyMessage(): string {
|
||||
if (this.code === 'validation_error' && this.details && typeof this.details === 'object') {
|
||||
const d = this.details as { issues?: { fieldErrors?: Record<string, string[]> } };
|
||||
const fields = d.issues?.fieldErrors;
|
||||
if (fields) {
|
||||
const parts = Object.entries(fields)
|
||||
.filter(([, msgs]) => msgs && msgs.length > 0)
|
||||
.map(([k, msgs]) => `${k}: ${msgs!.join(', ')}`);
|
||||
if (parts.length) return `Ошибка в полях: ${parts.join('; ')}`;
|
||||
}
|
||||
}
|
||||
return `${this.code} (HTTP ${this.status})`;
|
||||
}
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
|
||||
@@ -64,7 +64,7 @@ export function ClientsPage() {
|
||||
setEditing(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ export function DocumentEditPage() {
|
||||
setSavedAt(new Date());
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -185,7 +185,7 @@ export function DocumentEditPage() {
|
||||
await api.del(`/api/documents/${savedId}`);
|
||||
navigate('/documents');
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export function OrganizationPage() {
|
||||
setDraft(saved);
|
||||
setSavedAt(new Date());
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function ServicesPage() {
|
||||
setEditing(null);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export function TemplateEditPage() {
|
||||
await api.put(`/api/templates/${id}`, { name, docType, body });
|
||||
setSavedAt(new Date());
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiError ? `${e.code} (${e.status})` : String(e));
|
||||
setError(e instanceof ApiError ? e.prettyMessage() : String(e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user