fix(ui): extract meaningful error messages from API responses
Introduces ApiError class in client.ts that parses Spring Boot error bodies to extract human-readable messages (message, error, detail fields). Adds errorMessage() helper used by all toast descriptions instead of raw String(err) which dumped JSON blobs to the user. Affected: all 10 page components that display error toasts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,42 @@ export function setTokenProvider(provider: (() => Promise<string | undefined>) |
|
||||
tokenProvider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error class that extracts a human-readable message from API responses.
|
||||
* Spring Boot error bodies typically contain: { message, error, status, detail }
|
||||
* Our own controllers may return: { error, message, detail }
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
public readonly status: number;
|
||||
public readonly detail: string | null;
|
||||
|
||||
constructor(status: number, body: string) {
|
||||
const parsed = ApiError.parseBody(body);
|
||||
super(parsed.message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.detail = parsed.detail;
|
||||
}
|
||||
|
||||
private static parseBody(body: string): { message: string; detail: string | null } {
|
||||
try {
|
||||
const json = JSON.parse(body);
|
||||
// Try common Spring Boot / custom error fields in priority order
|
||||
const message = json.message || json.error || json.detail || json.reason || `Request failed`;
|
||||
const detail = json.detail || json.path || null;
|
||||
return { message, detail };
|
||||
} catch {
|
||||
// Not JSON — use raw text, truncated
|
||||
return { message: body.slice(0, 200) || 'Request failed', detail: null };
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns just the human message (no "Error:" prefix, no status code, no JSON blob). */
|
||||
toString(): string {
|
||||
return this.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = tokenProvider ? await tokenProvider() : null;
|
||||
@@ -24,20 +60,20 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
||||
if (response.status === 401) {
|
||||
// Don't hard-redirect — let React Query retry (token may not be ready yet).
|
||||
// The ProtectedRoute handles unauthenticated state.
|
||||
throw new Error('Unauthorized');
|
||||
throw new ApiError(401, '{"message":"Unauthorized"}');
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
const errorHeader = response.headers.get('X-Cameleer-Error');
|
||||
if (errorHeader === 'APP_MFA_REQUIRED') {
|
||||
window.location.href = '/platform/tenant/settings?mfa=required';
|
||||
throw new Error('MFA enrollment required');
|
||||
throw new ApiError(403, '{"message":"MFA enrollment required"}');
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`API error ${response.status}: ${text}`);
|
||||
throw new ApiError(response.status, text);
|
||||
}
|
||||
|
||||
if (response.status === 204) return undefined as T;
|
||||
@@ -57,3 +93,13 @@ export const api = {
|
||||
apiFetch<T>(path, { method: 'PUT', body }),
|
||||
delete: <T>(path: string) => apiFetch<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract a human-readable error message for toast display.
|
||||
* Works with ApiError (preferred) and plain Error/unknown.
|
||||
*/
|
||||
export function errorMessage(err: unknown): string {
|
||||
if (err instanceof ApiError) return err.message;
|
||||
if (err instanceof Error) return err.message;
|
||||
return String(err);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import { Card, Input, Button, FormField, Alert } from '@cameleer/design-system';
|
||||
import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
|
||||
import { api } from '../api/client';
|
||||
import { api, errorMessage } from '../api/client';
|
||||
import { toSlug } from '../utils/slug';
|
||||
import styles from './OnboardingPage.module.css';
|
||||
|
||||
@@ -56,7 +56,7 @@ export function OnboardingPage() {
|
||||
// auto-approves and redirects back with fresh tokens.
|
||||
await signIn(`${window.location.origin}/platform/callback`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const msg = err instanceof Error ? err.message : errorMessage(err);
|
||||
if (msg.includes('409')) {
|
||||
setError('This organization name is already taken. Try a different organization name.');
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -49,7 +50,7 @@ function MfaSection() {
|
||||
setSetupData(data);
|
||||
setVerifyCode('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to start MFA setup', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to start MFA setup', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ function MfaSection() {
|
||||
toast({ title: 'Invalid code. Please try again.', variant: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Verification failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Verification failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ function MfaSection() {
|
||||
setCodesSaved(false);
|
||||
toast({ title: 'Backup codes regenerated', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to regenerate backup codes', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to regenerate backup codes', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ function MfaSection() {
|
||||
setSetupData(null);
|
||||
toast({ title: 'MFA removed', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to remove MFA', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to remove MFA', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +288,7 @@ function MfaEnforcementToggle() {
|
||||
await updateSettings.mutateAsync({ mfaRequired: false });
|
||||
toast({ title: 'MFA requirement disabled for all members', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +298,7 @@ function MfaEnforcementToggle() {
|
||||
setConfirmEnable(false);
|
||||
toast({ title: 'MFA is now required for all members', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +366,7 @@ export function SettingsPage() {
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to change password', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to change password', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +476,7 @@ export function SettingsPage() {
|
||||
toast({ title: 'Server admin password reset successfully', variant: 'success' });
|
||||
setServerAdminPw('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to reset server admin password', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to reset server admin password', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert, AlertDialog, Badge, Button, Card, DataTable,
|
||||
EmptyState, FileInput, FormField, Input, Spinner, useToast,
|
||||
@@ -87,7 +88,7 @@ export function SsoPage() {
|
||||
toast({ title: `SSO connector "${connectorName}" created`, variant: 'success' });
|
||||
resetForm();
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to create connector', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to create connector', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ export function SsoPage() {
|
||||
toast({ title: `Deleted "${deleteTarget.connectorName}"`, variant: 'success' });
|
||||
setDeleteTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +113,7 @@ export function SsoPage() {
|
||||
toast({ title: 'Connection test failed', description: 'IdP metadata could not be resolved', variant: 'warning' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Connection test failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Connection test failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +352,7 @@ function CaCertificatesSection() {
|
||||
certRef.current?.clear();
|
||||
setLabel('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upload failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +361,7 @@ function CaCertificatesSection() {
|
||||
await activateMutation.mutateAsync(id);
|
||||
toast({ title: 'CA certificate activated', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Activation failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Activation failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ function CaCertificatesSection() {
|
||||
toast({ title: 'CA certificate removed', variant: 'success' });
|
||||
setDeleteTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
AlertDialog,
|
||||
@@ -131,7 +132,7 @@ export function TeamPage() {
|
||||
setInviteRole('viewer');
|
||||
setShowInvite(false);
|
||||
} catch (err) {
|
||||
toast({ title: 'Invite failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Invite failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ export function TeamPage() {
|
||||
toast({ title: `Removed ${removeTarget.name}`, variant: 'success' });
|
||||
setRemoveTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Remove failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Remove failed', description: errorMessage(err), variant: 'error' });
|
||||
setRemoveTarget(null);
|
||||
}
|
||||
}
|
||||
@@ -269,7 +270,7 @@ export function TeamPage() {
|
||||
toast({ title: `MFA reset for ${mfaResetTarget.name || mfaResetTarget.email}`, variant: 'success' });
|
||||
setMfaResetTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to reset MFA', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to reset MFA', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={resetMfa.isPending}
|
||||
@@ -297,7 +298,7 @@ export function TeamPage() {
|
||||
setPwTarget(null);
|
||||
setPwValue('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Reset failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Reset failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@cameleer/design-system';
|
||||
import { ArrowUpCircle, ExternalLink, Key, RefreshCw, Settings } from 'lucide-react';
|
||||
import { useTenantDashboard, useRestartServer, useUpgradeServer } from '../../api/tenant-hooks';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
||||
import { UsageIndicator } from '../../components/UsageIndicator';
|
||||
import { tierColor } from '../../utils/tier';
|
||||
@@ -107,7 +108,7 @@ export function TenantDashboardPage() {
|
||||
await restartServer.mutateAsync();
|
||||
toast({ title: 'Server restarted', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={restartServer.isPending}
|
||||
@@ -122,7 +123,7 @@ export function TenantDashboardPage() {
|
||||
await upgradeServer.mutateAsync();
|
||||
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={upgradeServer.isPending}
|
||||
|
||||
9
ui/src/pages/vendor/CertificatesPage.tsx
vendored
9
ui/src/pages/vendor/CertificatesPage.tsx
vendored
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -163,7 +164,7 @@ export function CertificatesPage() {
|
||||
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upload failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +173,7 @@ export function CertificatesPage() {
|
||||
await activateMutation.mutateAsync();
|
||||
toast({ title: 'Certificate activated', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Activation failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Activation failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ export function CertificatesPage() {
|
||||
await restoreMutation.mutateAsync();
|
||||
toast({ title: 'Certificate restored from archive', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Restore failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Restore failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ export function CertificatesPage() {
|
||||
await discardMutation.mutateAsync();
|
||||
toast({ title: 'Staged certificate discarded', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Discard failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Discard failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
3
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button, Card, FormField, Input, useToast } from '@cameleer/design-system';
|
||||
import { useCreateTenant } from '../../api/vendor-hooks';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import { toSlug } from '../../utils/slug';
|
||||
|
||||
const TIERS = ['STARTER', 'TEAM', 'BUSINESS', 'ENTERPRISE'];
|
||||
@@ -35,7 +36,7 @@ export function CreateTenantPage() {
|
||||
toast({ title: 'Tenant created — provisioning in progress', variant: 'success' });
|
||||
navigate(`/vendor/tenants/${result.id}`);
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to create tenant', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to create tenant', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
9
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
@@ -9,6 +9,7 @@ import {
|
||||
useToast,
|
||||
} from '@cameleer/design-system';
|
||||
import { Send, Trash2, Save, Power } from 'lucide-react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
useEmailConnector,
|
||||
useSaveEmailConnector,
|
||||
@@ -70,7 +71,7 @@ export function EmailConfigPage() {
|
||||
setEditing(false);
|
||||
setPassword('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to save', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to save', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ export function EmailConfigPage() {
|
||||
setConfirmDelete(false);
|
||||
setEditing(false);
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to delete', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to delete', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ export function EmailConfigPage() {
|
||||
toast({ title: 'Test failed', description: result.message, variant: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Test failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Test failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ export function EmailConfigPage() {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to toggle registration', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to toggle registration', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
ui/src/pages/vendor/LicenseVerifyPage.tsx
vendored
3
ui/src/pages/vendor/LicenseVerifyPage.tsx
vendored
@@ -7,6 +7,7 @@ import {
|
||||
} from '@cameleer/design-system';
|
||||
import { Clipboard, Search, ShieldCheck } from 'lucide-react';
|
||||
import { useVerifyLicense, usePublicKey } from '../../api/vendor-hooks';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import styles from '../../styles/platform.module.css';
|
||||
import type { VerifyLicenseResponse } from '../../types/api';
|
||||
|
||||
@@ -49,7 +50,7 @@ export function LicenseVerifyPage() {
|
||||
const res = await verifyLicense.mutateAsync(token.trim());
|
||||
setResult(res);
|
||||
} catch (err) {
|
||||
toast({ title: 'Verification failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Verification failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
ui/src/pages/vendor/TenantDetailPage.tsx
vendored
11
ui/src/pages/vendor/TenantDetailPage.tsx
vendored
@@ -20,6 +20,7 @@ import {
|
||||
useRestartServer,
|
||||
useUpgradeServer,
|
||||
} from '../../api/vendor-hooks';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
||||
import { tierColor } from '../../utils/tier';
|
||||
import styles from '../../styles/platform.module.css';
|
||||
@@ -155,7 +156,7 @@ export function TenantDetailPage() {
|
||||
variant: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
toast({ title: 'Minting failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Minting failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ export function TenantDetailPage() {
|
||||
toast({ title: 'Tenant suspended', variant: 'warning' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Action failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Action failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +211,7 @@ export function TenantDetailPage() {
|
||||
await restartServer.mutateAsync(id);
|
||||
toast({ title: 'Server restarted', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +221,7 @@ export function TenantDetailPage() {
|
||||
await upgradeServer.mutateAsync(id);
|
||||
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +232,7 @@ export function TenantDetailPage() {
|
||||
toast({ title: 'Tenant deleted', variant: 'success' });
|
||||
navigate('/vendor/tenants');
|
||||
} catch (err) {
|
||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user