treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
This commit is contained in:
parent
5d24bedd60
commit
39b95c2ede
203 changed files with 10090 additions and 6085 deletions
|
|
@ -26,15 +26,15 @@
|
|||
// URL params. `labelFilter` is `string[]`, not `Set<string>`: 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';
|
||||
import './IssueReportPage.css';
|
||||
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";
|
||||
import "./IssueReportPage.css";
|
||||
|
||||
interface IssueReportRow {
|
||||
repo: string;
|
||||
|
|
@ -53,49 +53,53 @@ interface IssueReportRow {
|
|||
}
|
||||
|
||||
type SortKey =
|
||||
| 'repo'
|
||||
| 'number'
|
||||
| 'title'
|
||||
| 'assignees'
|
||||
| 'blocked'
|
||||
| 'depended_on_by_count'
|
||||
| 'transitively_blocks_count';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
| "repo"
|
||||
| "number"
|
||||
| "title"
|
||||
| "assignees"
|
||||
| "blocked"
|
||||
| "depended_on_by_count"
|
||||
| "transitively_blocks_count";
|
||||
type SortDir = "asc" | "desc";
|
||||
|
||||
// Sentinel `<select>` value for "every repo" — `''` can't collide with a
|
||||
// real `owner/name` value, which always contains a slash.
|
||||
const ALL_REPOS = '';
|
||||
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';
|
||||
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('/');
|
||||
const i = repo.indexOf("/");
|
||||
if (i < 0) return null;
|
||||
return { org: repo.slice(0, i), name: repo.slice(i + 1) };
|
||||
}
|
||||
|
||||
function compareRows(a: IssueReportRow, b: IssueReportRow, key: SortKey): number {
|
||||
function compareRows(
|
||||
a: IssueReportRow,
|
||||
b: IssueReportRow,
|
||||
key: SortKey,
|
||||
): number {
|
||||
switch (key) {
|
||||
case 'repo':
|
||||
case "repo":
|
||||
return a.repo.localeCompare(b.repo);
|
||||
case 'number':
|
||||
case "number":
|
||||
return a.number - b.number;
|
||||
case 'title':
|
||||
case "title":
|
||||
return a.title.localeCompare(b.title);
|
||||
case 'assignees':
|
||||
return a.assignees.join(', ').localeCompare(b.assignees.join(', '));
|
||||
case 'blocked':
|
||||
case "assignees":
|
||||
return a.assignees.join(", ").localeCompare(b.assignees.join(", "));
|
||||
case "blocked":
|
||||
return Number(a.blocked) - Number(b.blocked);
|
||||
case 'depended_on_by_count':
|
||||
case "depended_on_by_count":
|
||||
return a.depended_on_by_count - b.depended_on_by_count;
|
||||
case 'transitively_blocks_count':
|
||||
case "transitively_blocks_count":
|
||||
return a.transitively_blocks_count - b.transitively_blocks_count;
|
||||
}
|
||||
}
|
||||
|
|
@ -115,23 +119,41 @@ function SortHeader({
|
|||
}) {
|
||||
const active = sortKey === activeKey;
|
||||
return (
|
||||
<button type="button" class="issue-report-sort-btn" onClick={() => onSort(sortKey)}>
|
||||
<button
|
||||
type="button"
|
||||
class="issue-report-sort-btn"
|
||||
onClick={() => onSort(sortKey)}
|
||||
>
|
||||
{label}
|
||||
{active ? <span aria-hidden="true"> {dir === 'asc' ? '▲' : '▼'}</span> : null}
|
||||
{active ? (
|
||||
<span aria-hidden="true"> {dir === "asc" ? "▲" : "▼"}</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function IssueReportPage() {
|
||||
const [repos, setRepos] = useState<string[] | null>(null);
|
||||
const [repoFilter, setRepoFilter] = useLocalSetting(REPO_FILTER_KEY, ALL_REPOS);
|
||||
const [repoFilter, setRepoFilter] = useLocalSetting(
|
||||
REPO_FILTER_KEY,
|
||||
ALL_REPOS,
|
||||
);
|
||||
const [rows, setRows] = useState<IssueReportRow[] | null>(null);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hideBlocked, setHideBlocked] = useLocalSetting(HIDE_BLOCKED_KEY, false);
|
||||
const [labelFilter, setLabelFilter] = useLocalSetting<string[]>(LABEL_FILTER_KEY, []);
|
||||
const [sortKey, setSortKey] = useLocalSetting<SortKey>(SORT_KEY_KEY, 'depended_on_by_count');
|
||||
const [sortDir, setSortDir] = useLocalSetting<SortDir>(SORT_DIR_KEY, 'desc');
|
||||
const [hideBlocked, setHideBlocked] = useLocalSetting(
|
||||
HIDE_BLOCKED_KEY,
|
||||
false,
|
||||
);
|
||||
const [labelFilter, setLabelFilter] = useLocalSetting<string[]>(
|
||||
LABEL_FILTER_KEY,
|
||||
[],
|
||||
);
|
||||
const [sortKey, setSortKey] = useLocalSetting<SortKey>(
|
||||
SORT_KEY_KEY,
|
||||
"depended_on_by_count",
|
||||
);
|
||||
const [sortDir, setSortDir] = useLocalSetting<SortDir>(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
|
||||
|
|
@ -142,7 +164,7 @@ export function IssueReportPage() {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const r = await fetch('/api/repos');
|
||||
const r = await fetch("/api/repos");
|
||||
if (!r.ok) {
|
||||
if (!cancelled) setError(await readApiError(r));
|
||||
return;
|
||||
|
|
@ -163,7 +185,9 @@ export function IssueReportPage() {
|
|||
let cancelled = false;
|
||||
setLoading(true);
|
||||
const split = repoFilter ? splitRepo(repoFilter) : null;
|
||||
const url = split ? `/api/repos/${split.org}/${split.name}/issue-report` : '/api/issue-report';
|
||||
const url = split
|
||||
? `/api/repos/${split.org}/${split.name}/issue-report`
|
||||
: "/api/issue-report";
|
||||
(async () => {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
|
|
@ -199,25 +223,28 @@ export function IssueReportPage() {
|
|||
const visibleRows = useMemo(() => {
|
||||
let out = rows ?? [];
|
||||
if (hideBlocked) out = out.filter((r) => !r.blocked);
|
||||
if (labelFilter.length > 0) out = out.filter((r) => r.labels.some((l) => labelFilter.includes(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;
|
||||
return sortDir === "asc" ? c : -c;
|
||||
});
|
||||
}, [rows, hideBlocked, labelFilter, sortKey, sortDir]);
|
||||
|
||||
function onSort(key: SortKey) {
|
||||
if (key === sortKey) {
|
||||
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
||||
setSortDir(sortDir === "asc" ? "desc" : "asc");
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir('asc');
|
||||
setSortDir("asc");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLabel(label: string) {
|
||||
setLabelFilter(
|
||||
labelFilter.includes(label) ? labelFilter.filter((l) => l !== label) : [...labelFilter, label],
|
||||
labelFilter.includes(label)
|
||||
? labelFilter.filter((l) => l !== label)
|
||||
: [...labelFilter, label],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -225,9 +252,9 @@ export function IssueReportPage() {
|
|||
// but not the active column, the real direction while it is. Screen
|
||||
// readers announce this; the ▲/▼ glyph in `SortHeader` is `aria-hidden`
|
||||
// and carries no information without it.
|
||||
function ariaSortFor(key: SortKey): 'ascending' | 'descending' | 'none' {
|
||||
if (sortKey !== key) return 'none';
|
||||
return sortDir === 'asc' ? 'ascending' : 'descending';
|
||||
function ariaSortFor(key: SortKey): "ascending" | "descending" | "none" {
|
||||
if (sortKey !== key) return "none";
|
||||
return sortDir === "asc" ? "ascending" : "descending";
|
||||
}
|
||||
|
||||
const columns: TableColumn<IssueReportRow>[] = [
|
||||
|
|
@ -235,15 +262,31 @@ export function IssueReportPage() {
|
|||
// a fixed column set means the table's shape doesn't shift under
|
||||
// sort/filter state, and it's a free confirmation of what's loaded.
|
||||
{
|
||||
key: 'repo',
|
||||
header: <SortHeader label="repo" sortKey="repo" activeKey={sortKey} dir={sortDir} onSort={onSort} />,
|
||||
ariaSort: ariaSortFor('repo'),
|
||||
key: "repo",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="repo"
|
||||
sortKey="repo"
|
||||
activeKey={sortKey}
|
||||
dir={sortDir}
|
||||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor("repo"),
|
||||
render: (r) => r.repo,
|
||||
},
|
||||
{
|
||||
key: 'number',
|
||||
header: <SortHeader label="issue" sortKey="number" activeKey={sortKey} dir={sortDir} onSort={onSort} />,
|
||||
ariaSort: ariaSortFor('number'),
|
||||
key: "number",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="issue"
|
||||
sortKey="number"
|
||||
activeKey={sortKey}
|
||||
dir={sortDir}
|
||||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor("number"),
|
||||
render: (r) =>
|
||||
r.html_url ? (
|
||||
<a href={r.html_url} target="_blank" rel="noreferrer">
|
||||
|
|
@ -254,32 +297,55 @@ export function IssueReportPage() {
|
|||
),
|
||||
},
|
||||
{
|
||||
key: 'title',
|
||||
header: <SortHeader label="title" sortKey="title" activeKey={sortKey} dir={sortDir} onSort={onSort} />,
|
||||
ariaSort: ariaSortFor('title'),
|
||||
key: "title",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="title"
|
||||
sortKey="title"
|
||||
activeKey={sortKey}
|
||||
dir={sortDir}
|
||||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor("title"),
|
||||
render: (r) => r.title,
|
||||
},
|
||||
{
|
||||
key: 'labels',
|
||||
header: 'labels',
|
||||
render: (r) => (r.labels.length ? r.labels.join(', ') : '—'),
|
||||
key: "labels",
|
||||
header: "labels",
|
||||
render: (r) => (r.labels.length ? r.labels.join(", ") : "—"),
|
||||
},
|
||||
{
|
||||
key: 'assignees',
|
||||
key: "assignees",
|
||||
header: (
|
||||
<SortHeader label="assignees" sortKey="assignees" activeKey={sortKey} dir={sortDir} onSort={onSort} />
|
||||
<SortHeader
|
||||
label="assignees"
|
||||
sortKey="assignees"
|
||||
activeKey={sortKey}
|
||||
dir={sortDir}
|
||||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor('assignees'),
|
||||
render: (r) => (r.assignees.length ? r.assignees.join(', ') : '—'),
|
||||
ariaSort: ariaSortFor("assignees"),
|
||||
render: (r) => (r.assignees.length ? r.assignees.join(", ") : "—"),
|
||||
},
|
||||
{
|
||||
key: 'blocked',
|
||||
header: <SortHeader label="blocked" sortKey="blocked" activeKey={sortKey} dir={sortDir} onSort={onSort} />,
|
||||
ariaSort: ariaSortFor('blocked'),
|
||||
render: (r) => (r.blocked ? <Badge tone="warning" value="blocked" /> : '—'),
|
||||
key: "blocked",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="blocked"
|
||||
sortKey="blocked"
|
||||
activeKey={sortKey}
|
||||
dir={sortDir}
|
||||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor("blocked"),
|
||||
render: (r) =>
|
||||
r.blocked ? <Badge tone="warning" value="blocked" /> : "—",
|
||||
},
|
||||
{
|
||||
key: 'depended_on_by_count',
|
||||
key: "depended_on_by_count",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="depended on by"
|
||||
|
|
@ -289,11 +355,11 @@ export function IssueReportPage() {
|
|||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor('depended_on_by_count'),
|
||||
ariaSort: ariaSortFor("depended_on_by_count"),
|
||||
render: (r) => r.depended_on_by_count,
|
||||
},
|
||||
{
|
||||
key: 'transitively_blocks_count',
|
||||
key: "transitively_blocks_count",
|
||||
header: (
|
||||
<SortHeader
|
||||
label="transitively blocks"
|
||||
|
|
@ -303,7 +369,7 @@ export function IssueReportPage() {
|
|||
onSort={onSort}
|
||||
/>
|
||||
),
|
||||
ariaSort: ariaSortFor('transitively_blocks_count'),
|
||||
ariaSort: ariaSortFor("transitively_blocks_count"),
|
||||
render: (r) => r.transitively_blocks_count,
|
||||
},
|
||||
];
|
||||
|
|
@ -317,7 +383,7 @@ export function IssueReportPage() {
|
|||
value={repoFilter}
|
||||
onChange={setRepoFilter}
|
||||
options={[
|
||||
{ value: ALL_REPOS, label: 'all repos' },
|
||||
{ value: ALL_REPOS, label: "all repos" },
|
||||
...(repos ?? []).map((r) => ({ value: r, label: r })),
|
||||
]}
|
||||
/>
|
||||
|
|
@ -325,7 +391,9 @@ export function IssueReportPage() {
|
|||
<input
|
||||
type="checkbox"
|
||||
checked={hideBlocked}
|
||||
onChange={(e) => setHideBlocked((e.target as HTMLInputElement).checked)}
|
||||
onChange={(e) =>
|
||||
setHideBlocked((e.target as HTMLInputElement).checked)
|
||||
}
|
||||
/>
|
||||
hide blocked (open dependency)
|
||||
</label>
|
||||
|
|
@ -334,13 +402,22 @@ export function IssueReportPage() {
|
|||
<div class="issue-report-labels">
|
||||
{allLabels.map((l) => (
|
||||
<label key={l} class="issue-report-label-chip">
|
||||
<input type="checkbox" checked={labelFilter.includes(l)} onChange={() => toggleLabel(l)} />
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={labelFilter.includes(l)}
|
||||
onChange={() => toggleLabel(l)}
|
||||
/>
|
||||
{l}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{error ? <ApiErrorPanel context="failed to load the issue report" problem={error} /> : null}
|
||||
{error ? (
|
||||
<ApiErrorPanel
|
||||
context="failed to load the issue report"
|
||||
problem={error}
|
||||
/>
|
||||
) : null}
|
||||
{!error && loading ? <p>generating report…</p> : null}
|
||||
{!loading && rows ? (
|
||||
<Table
|
||||
|
|
|
|||
Loading…
Reference in a new issue