diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx index 0f9b8d33..df865e2c 100644 --- a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx @@ -20,10 +20,17 @@ // (mara: a "generating report" spinner sub-5s is fine, doesn't need to // be instant), so it fetches once per repo-selection change instead of // polling. +// +// Sort/filter state persists via the shared `useLocalSetting` hook (same +// plumbing the theme/motion overrides use) — mara: "local storage" over +// URL params. `labelFilter` is `string[]`, not `Set`: a `Set` +// serializes to `"{}"` through `JSON.stringify` and silently loses its +// contents, which is exactly what this hook round-trips through. import { useEffect, useMemo, useState } from 'preact/hooks'; import { ApiErrorPanel } from '@hive/shared/api-error-panel.js'; import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js'; import { Badge } from '@hive/shared/badge.js'; +import { useLocalSetting } from '@hive/shared/settings-storage.js'; import { Panel } from '../ui/panel/Panel.js'; import { SelectField } from '../ui/select-field/SelectField.js'; import { Table, type TableColumn } from '../ui/table/Table.js'; @@ -59,6 +66,15 @@ type SortDir = 'asc' | 'desc'; // real `owner/name` value, which always contains a slash. const ALL_REPOS = ''; +// One key per persisted control, namespaced like the theme/motion keys +// (`swarm-ui:issue-report:…`) so nothing else on the page — or a future +// page — collides with these by accident. +const REPO_FILTER_KEY = 'swarm-ui:issue-report:repo-filter'; +const HIDE_BLOCKED_KEY = 'swarm-ui:issue-report:hide-blocked'; +const LABEL_FILTER_KEY = 'swarm-ui:issue-report:label-filter'; +const SORT_KEY_KEY = 'swarm-ui:issue-report:sort-key'; +const SORT_DIR_KEY = 'swarm-ui:issue-report:sort-dir'; + function splitRepo(repo: string): { org: string; name: string } | null { const i = repo.indexOf('/'); if (i < 0) return null; @@ -108,14 +124,14 @@ function SortHeader({ export function IssueReportPage() { const [repos, setRepos] = useState(null); - const [repoFilter, setRepoFilter] = useState(ALL_REPOS); + const [repoFilter, setRepoFilter] = useLocalSetting(REPO_FILTER_KEY, ALL_REPOS); const [rows, setRows] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - const [hideBlocked, setHideBlocked] = useState(false); - const [labelFilter, setLabelFilter] = useState>(new Set()); - const [sortKey, setSortKey] = useState('depended_on_by_count'); - const [sortDir, setSortDir] = useState('desc'); + const [hideBlocked, setHideBlocked] = useLocalSetting(HIDE_BLOCKED_KEY, false); + const [labelFilter, setLabelFilter] = useLocalSetting(LABEL_FILTER_KEY, []); + const [sortKey, setSortKey] = useLocalSetting(SORT_KEY_KEY, 'depended_on_by_count'); + const [sortDir, setSortDir] = useLocalSetting(SORT_DIR_KEY, 'desc'); // Repo dropdown source, fetched once — this page has no refresh // cadence, and a repo gaining/losing its first/last open issue between @@ -183,7 +199,7 @@ export function IssueReportPage() { const visibleRows = useMemo(() => { let out = rows ?? []; if (hideBlocked) out = out.filter((r) => !r.blocked); - if (labelFilter.size > 0) out = out.filter((r) => r.labels.some((l) => labelFilter.has(l))); + if (labelFilter.length > 0) out = out.filter((r) => r.labels.some((l) => labelFilter.includes(l))); return [...out].sort((a, b) => { const c = compareRows(a, b, sortKey); return sortDir === 'asc' ? c : -c; @@ -192,7 +208,7 @@ export function IssueReportPage() { function onSort(key: SortKey) { if (key === sortKey) { - setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); + setSortDir(sortDir === 'asc' ? 'desc' : 'asc'); } else { setSortKey(key); setSortDir('asc'); @@ -200,12 +216,9 @@ export function IssueReportPage() { } function toggleLabel(label: string) { - setLabelFilter((prev) => { - const next = new Set(prev); - if (next.has(label)) next.delete(label); - else next.add(label); - return next; - }); + setLabelFilter( + labelFilter.includes(label) ? labelFilter.filter((l) => l !== label) : [...labelFilter, label], + ); } // `aria-sort` for a sortable column's `` — 'none' while sortable @@ -321,7 +334,7 @@ export function IssueReportPage() {
{allLabels.map((l) => ( ))}