Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/e2e/settings-request-budget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3219,3 +3219,19 @@ test('Refreshing local or Drive backup history while Load more is in flight igno
await expect(remoteRows.nth(5)).toHaveAttribute('data-backup-id', allRemoteBackups[5].id);
await expect(remoteRows.nth(14)).toHaveAttribute('data-backup-id', allRemoteBackups[14].id);
});

test('Counters tab reports a failed counters load instead of showing an empty list', async ({
page,
}) => {
await startMockedSettingsSession(page);
await page.route('**/api/counters**', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'unavailable' }),
});
});

await page.goto(`${BASE}/settings?tab=counters`);
await expect(page.getByText('Failed to load counters.', { exact: true }).first()).toBeVisible();
});
75 changes: 53 additions & 22 deletions frontend/src/components/settings/CountersSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function CountersSettingsTab({ isOwner }: Props) {
const [savingEnabled, setSavingEnabled] = useState(false);
const [counters, setCounters] = useState<CounterItem[]>([]);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState(false);

// Add / Edit Modal state
const [modalOpen, setModalOpen] = useState(false);
Expand All @@ -62,18 +63,30 @@ export function CountersSettingsTab({ isOwner }: Props) {
const [deletingId, setDeletingId] = useState<string | null>(null);

const loadData = useCallback(async () => {
setLoading(true);
try {
const [settingRes, countersRes] = await Promise.all([
api
.get('/settings/multi_counter_enabled')
.catch(() => ({ data: { setting: { value: 'false' } } })),
api
.get('/counters', { params: { include_inactive: 'true' } })
.catch(() => ({ data: { counters: [] } })),
const [settingResult, countersResult] = await Promise.allSettled([
api.get('/settings/multi_counter_enabled').catch((err: unknown) => {
if ((err as { response?: { status?: number } })?.response?.status === 404) {
return { data: { setting: { value: 'false' } } };
}
throw err;
}),
api.get('/counters', { params: { include_inactive: 'true' } }),
]);
setEnabled(settingRes.data?.setting?.value === 'true');
setCounters(countersRes.data?.counters || []);
if (settingResult.status === 'fulfilled') {
setEnabled(settingResult.value.data?.setting?.value === 'true');
}
if (countersResult.status === 'fulfilled') {
setCounters(countersResult.value.data?.counters || []);
}
const failed = settingResult.status === 'rejected' || countersResult.status === 'rejected';
setLoadError(failed);
if (failed) {
toast.error(t('counterLoadFailed'));
}
} catch {
setLoadError(true);
toast.error(t('counterLoadFailed'));
} finally {
setLoading(false);
Expand All @@ -82,22 +95,33 @@ export function CountersSettingsTab({ isOwner }: Props) {

useEffect(() => {
let cancelled = false;
Promise.all([
api
.get('/settings/multi_counter_enabled')
.catch(() => ({ data: { setting: { value: 'false' } } })),
api
.get('/counters', { params: { include_inactive: 'true' } })
.catch(() => ({ data: { counters: [] } })),
Promise.allSettled([
api.get('/settings/multi_counter_enabled').catch((err: unknown) => {
if ((err as { response?: { status?: number } })?.response?.status === 404) {
return { data: { setting: { value: 'false' } } };
}
throw err;
}),
api.get('/counters', { params: { include_inactive: 'true' } }),
])
.then(([settingRes, countersRes]) => {
.then(([settingResult, countersResult]) => {
if (cancelled) return;
setEnabled(settingRes.data?.setting?.value === 'true');
setCounters(countersRes.data?.counters || []);
if (settingResult.status === 'fulfilled') {
setEnabled(settingResult.value.data?.setting?.value === 'true');
}
if (countersResult.status === 'fulfilled') {
setCounters(countersResult.value.data?.counters || []);
}
const failed = settingResult.status === 'rejected' || countersResult.status === 'rejected';
setLoadError(failed);
if (failed) {
toast.error(t('counterLoadFailed'));
}
setLoading(false);
})
.catch(() => {
if (cancelled) return;
setLoadError(true);
toast.error(t('counterLoadFailed'));
setLoading(false);
});
Expand Down Expand Up @@ -227,15 +251,15 @@ export function CountersSettingsTab({ isOwner }: Props) {
</div>
<Toggle
value={enabled}
disabled={savingEnabled || !isOwner}
disabled={savingEnabled || loadError || !isOwner}
label={t('multiCounterTitle')}
onChange={handleToggleEnabled}
/>
</div>
</div>

{/* Counter management section */}
{enabled && (
{(enabled || loadError) && (
<div className="bg-card rounded-xl border border-border p-6 shadow-sm space-y-4">
<div className="flex items-center justify-between">
<div>
Expand All @@ -244,7 +268,7 @@ export function CountersSettingsTab({ isOwner }: Props) {
</div>
<Button
onClick={handleOpenAddModal}
disabled={!isOwner}
disabled={!isOwner || loadError}
size="sm"
className="gap-1.5"
>
Expand All @@ -258,6 +282,13 @@ export function CountersSettingsTab({ isOwner }: Props) {
<Loader2 size={18} className="animate-spin" />
<span>{tCommon('loading')}</span>
</div>
) : loadError ? (
<div className="text-center py-12 border border-dashed border-border rounded-lg text-muted-foreground space-y-3">
<p className="font-medium">{t('counterLoadFailed')}</p>
<Button variant="outline" size="sm" onClick={() => void loadData()}>
{t('retry')}
</Button>
</div>
) : counters.length === 0 ? (
<div className="text-center py-12 border border-dashed border-border rounded-lg text-muted-foreground">
<p className="font-medium">{t('noCountersConfigured')}</p>
Expand Down
Loading