Layout reaches zero. The dock, header, user menu, theme switcher, spotlight, shortcuts modal, screen-error fallback, rescan and bug report. Three things worth knowing: CORE_DOCK_ITEMS now carries labelKey instead of label, and one useDockItems() hook resolves the composed list. DashboardLayout, Spotlight and DockSettings each spread the same two sources before, so each would have had to resolve labels itself. Plugin tiles keep the name from their own manifest — a plugin's strings are not ours. Spotlight resolves labels INSIDE the hook, so rank() still matches what the reader can see rather than a dot-path. Its to: '/settings/...' literals are untouched, which is what docs:check parses — verified still 20 offered, 0 missing. The rescan toast said 'N skill(s)'. That is English giving up on a plural, so it is now a real plural entry per noun and reads correctly at 0, 1 and 2 in both locales. ThemeSwitcher used t as its theme loop variable; renamed to entry.
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { AlertTriangle } from 'lucide-react';
|
|
import { useClient } from 'hooks/useClient';
|
|
import { Button } from '@/components/ui/button';
|
|
import { useTranslation } from '@/lib/i18n';
|
|
|
|
type ScreenErrorFallbackProps = { error: Error; reset: () => void };
|
|
|
|
/**
|
|
* What the owner sees instead of a white screen.
|
|
*
|
|
* The recovery that matters is the last one. Every screen in the app renders a Workspace whose layout is
|
|
* a jsonb blob written by the client and never validated on the way back in, so a malformed one throws
|
|
* during render — and until this existed, the only way out was a psql session. Dashboards are left alone:
|
|
* they are user-created and hold content, whereas a screen's layout is just where its panes sit.
|
|
*/
|
|
export const ScreenErrorFallback = ({ error, reset }: ScreenErrorFallbackProps) => {
|
|
const client = useClient();
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
const [confirming, setConfirming] = useState(false);
|
|
const [resetting, setResetting] = useState(false);
|
|
|
|
const resetScreenLayouts = async () => {
|
|
if (!confirming) return setConfirming(true);
|
|
setResetting(true);
|
|
try {
|
|
const state = await client.get<Record<string, unknown>>('/dashboards');
|
|
const patch = Object.fromEntries(
|
|
Object.keys(state)
|
|
.filter((key) => key.startsWith('screens/'))
|
|
.map((key) => [key, null]),
|
|
);
|
|
if (Object.keys(patch).length > 0) await client.patch('/dashboards', patch);
|
|
queryClient.removeQueries({ queryKey: ['DASHBOARD_STATE'] });
|
|
window.location.reload();
|
|
} catch (err) {
|
|
console.error('[screen-error] could not reset screen layouts', err);
|
|
setResetting(false);
|
|
setConfirming(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="mx-auto flex h-full max-w-2xl flex-col justify-center gap-6 p-6">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="mt-1 size-6 shrink-0 text-amber-500" />
|
|
<div className="space-y-1">
|
|
<h1 className="text-xl font-semibold">{t('screenError.title')}</h1>
|
|
<p className="text-muted-foreground text-sm">{t('screenError.description')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<details className="bg-muted/40 rounded-md border p-3 text-sm">
|
|
<summary className="cursor-pointer select-none font-medium">
|
|
{error.message || t('screenError.unknownError')}
|
|
</summary>
|
|
<pre className="mt-2 max-h-64 overflow-auto whitespace-pre-wrap text-xs opacity-70">{error.stack}</pre>
|
|
</details>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button onClick={reset}>{t('screenError.tryAgain')}</Button>
|
|
<Button variant="secondary" onClick={() => window.location.reload()}>
|
|
{t('screenError.reload')}
|
|
</Button>
|
|
<Button variant="secondary" asChild>
|
|
<Link to="/">{t('screenError.goHome')}</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2 border-t pt-4">
|
|
<p className="text-muted-foreground text-sm">
|
|
{t('screenError.resetHint')} <strong>{t('screenError.dashboardsSafe')}</strong>
|
|
</p>
|
|
<Button variant={confirming ? 'destructive' : 'outline'} disabled={resetting} onClick={resetScreenLayouts}>
|
|
{resetting ? t('screenError.resetting') : confirming ? t('screenError.resetConfirm') : t('screenError.reset')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|