From 858448512e5ab7e77f72bade5967b38641a021ef Mon Sep 17 00:00:00 2001 From: lizerk Date: Tue, 14 Apr 2026 02:10:20 +0300 Subject: [PATCH] fix(frontend): parse department api validation errors --- .../src/components/departments/api-error.js | 29 +++++++++++++++ .../components/departments/api-error.test.js | 35 +++++++++++++++++++ frontend/src/hooks/useDepartmentPage.ts | 27 ++------------ 3 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/departments/api-error.js create mode 100644 frontend/src/components/departments/api-error.test.js diff --git a/frontend/src/components/departments/api-error.js b/frontend/src/components/departments/api-error.js new file mode 100644 index 00000000..8db86a5a --- /dev/null +++ b/frontend/src/components/departments/api-error.js @@ -0,0 +1,29 @@ +export function extractDepartmentApiErrorMessage(error, fallback) { + const raw = String(error?.message || fallback || "").trim(); + const prefix = "API Error:"; + if (!raw.startsWith(prefix)) return raw || fallback; + + const payload = raw.slice(prefix.length).trim(); + const jsonStart = payload.indexOf("{"); + if (jsonStart >= 0) { + try { + const parsed = JSON.parse(payload.slice(jsonStart)); + const detail = parsed.detail; + if (typeof detail === "string" && detail.trim()) return detail; + const firstEntry = Object.entries(parsed)[0]; + if (firstEntry) { + const value = firstEntry[1]; + if (Array.isArray(value) && value[0]) return String(value[0]); + if (typeof value === "string" && value.trim()) return value; + } + } catch { + return fallback; + } + } + + const plainMessage = payload.replace(/^\d+\s+/, "").trim(); + if (/^\d+$/.test(plainMessage || payload)) { + return fallback; + } + return plainMessage || fallback; +} diff --git a/frontend/src/components/departments/api-error.test.js b/frontend/src/components/departments/api-error.test.js new file mode 100644 index 00000000..129a18d3 --- /dev/null +++ b/frontend/src/components/departments/api-error.test.js @@ -0,0 +1,35 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { extractDepartmentApiErrorMessage } from "./api-error.js"; + +test("extracts plain API error text after status code", () => { + const error = new Error( + "API Error: 400 Employee already belongs to another active department: Бухгалтерия.", + ); + + assert.equal( + extractDepartmentApiErrorMessage(error, "fallback"), + "Employee already belongs to another active department: Бухгалтерия.", + ); +}); + +test("extracts first field error from JSON payload", () => { + const error = new Error( + 'API Error: 400 {"employee_id":["Employee already belongs to another active department: Бухгалтерия."]}', + ); + + assert.equal( + extractDepartmentApiErrorMessage(error, "fallback"), + "Employee already belongs to another active department: Бухгалтерия.", + ); +}); + +test("falls back for unparsable payload", () => { + const error = new Error("API Error: 500"); + + assert.equal( + extractDepartmentApiErrorMessage(error, "fallback"), + "fallback", + ); +}); diff --git a/frontend/src/hooks/useDepartmentPage.ts b/frontend/src/hooks/useDepartmentPage.ts index e1ab6b64..71260b69 100644 --- a/frontend/src/hooks/useDepartmentPage.ts +++ b/frontend/src/hooks/useDepartmentPage.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { extractDepartmentApiErrorMessage } from "@/components/departments/api-error"; import { getDepartmentMemberModalError } from "@/components/departments/member-modal-state"; import { useUser } from "@/contexts/UserContext"; import { apiClient } from "@/lib/api"; @@ -96,31 +97,7 @@ const EMPTY_USER_PERMS: DepartmentUserPermissions = { can_assign_roles: false, }; -function getErrorMessage(error: unknown, fallback: string) { - const raw = String((error as Error)?.message || fallback).trim(); - const prefix = "API Error:"; - if (!raw.startsWith(prefix)) return raw || fallback; - - const payload = raw.slice(prefix.length).trim(); - const jsonStart = payload.indexOf("{"); - if (jsonStart >= 0) { - try { - const parsed = JSON.parse(payload.slice(jsonStart)) as Record; - const detail = parsed.detail; - if (typeof detail === "string" && detail.trim()) return detail; - const firstEntry = Object.entries(parsed)[0]; - if (firstEntry) { - const value = firstEntry[1]; - if (Array.isArray(value) && value[0]) return String(value[0]); - if (typeof value === "string" && value.trim()) return value; - } - } catch { - return fallback; - } - } - - return fallback; -} +const getErrorMessage = extractDepartmentApiErrorMessage; function sortEmployeesByName(items: User[]) { return [...items].sort((a, b) => {