feat(web): collapse Skipped→Archived, Partial→Paused+Failed; full-width filter rows
- Activity filter tabs drop Partial and Skipped; Partial runs now appear under both Paused and Failed (anything that didn't fully succeed), Skipped runs surface under Archived (history the operator chose not to send). Five tabs left: All / Success / Paused / Failed / Archived. - listActivityRuns flips skipped runs out of the default list and into the archived view at the SQL layer so pagination stays correct. - Tabs row spans the full width and wraps onto a second row when the viewport can't fit them. Account-filter select also span full width on every breakpoint instead of capping at sm:max-w-xs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ebbbdbdfb8
commit
797326e062
@ -106,24 +106,25 @@ function RunStatusBadge({ status }: { status: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
type FilterValue =
|
||||
| "all"
|
||||
| "success"
|
||||
| "paused"
|
||||
| "partial"
|
||||
| "failed"
|
||||
| "skipped"
|
||||
| "archived";
|
||||
type FilterValue = "all" | "success" | "paused" | "failed" | "archived";
|
||||
const FILTER_TABS: { value: FilterValue; label: string }[] = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "success", label: "Success" },
|
||||
{ value: "paused", label: "Paused" },
|
||||
{ value: "partial", label: "Partial" },
|
||||
{ value: "failed", label: "Failed" },
|
||||
{ value: "skipped", label: "Skipped" },
|
||||
{ value: "archived", label: "Archived" },
|
||||
];
|
||||
|
||||
// Partial runs (some recipients ok, some failed) surface under BOTH the
|
||||
// Paused and Failed tabs — the operator wants to see anything that didn't
|
||||
// fully succeed on either page. Skipped runs collapse into Archived since
|
||||
// they're effectively "history that the operator chose not to send".
|
||||
const FILTER_STATUSES: Record<Exclude<FilterValue, "all" | "archived">, string[]> = {
|
||||
success: ["success"],
|
||||
paused: ["paused", "partial"],
|
||||
failed: ["failed", "partial"],
|
||||
};
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ filter?: string }>;
|
||||
}
|
||||
@ -185,9 +186,7 @@ export default async function ActivityPage({ searchParams }: PageProps) {
|
||||
const filter: FilterValue =
|
||||
sp.filter === "success" ||
|
||||
sp.filter === "paused" ||
|
||||
sp.filter === "partial" ||
|
||||
sp.filter === "failed" ||
|
||||
sp.filter === "skipped" ||
|
||||
sp.filter === "archived"
|
||||
? sp.filter
|
||||
: "all";
|
||||
@ -198,7 +197,7 @@ export default async function ActivityPage({ searchParams }: PageProps) {
|
||||
const filtered =
|
||||
filter === "all" || filter === "archived"
|
||||
? runs
|
||||
: runs.filter((r) => r.status === filter);
|
||||
: runs.filter((r) => FILTER_STATUSES[filter].includes(r.status));
|
||||
const hasAny = runs.length > 0;
|
||||
|
||||
return (
|
||||
@ -235,18 +234,18 @@ export default async function ActivityPage({ searchParams }: PageProps) {
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{/* Six tabs (All / Success / Partial / Failed / Skipped / Archived)
|
||||
packed into a phone-width row left every label squeezed to
|
||||
~50px. Wrap the list in an overflow-x scroller so each tab
|
||||
keeps a readable label + comfortable touch target on mobile;
|
||||
on desktop the row fits naturally and no scroll bar appears.
|
||||
Negative margins extend the scroller to the page edges so the
|
||||
first/last tabs don't look clipped against the container. */}
|
||||
{/* Filter tabs span the full row and wrap onto a second line when the
|
||||
viewport can't fit them all. Each trigger has a small basis so they
|
||||
share space evenly while still keeping a readable label on mobile. */}
|
||||
<Tabs value={filter}>
|
||||
<div className="-mx-4 overflow-x-auto px-4 sm:mx-0 sm:px-0 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||
<TabsList>
|
||||
<TabsList className="flex w-full flex-wrap gap-1 group-data-horizontal/tabs:h-auto p-1">
|
||||
{FILTER_TABS.map(({ value, label }) => (
|
||||
<TabsTrigger key={value} value={value} asChild>
|
||||
<TabsTrigger
|
||||
key={value}
|
||||
value={value}
|
||||
asChild
|
||||
className="h-8 grow basis-20"
|
||||
>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
<Link href={(value === "all" ? "/activity" : `/activity?filter=${value}`) as any}>
|
||||
{label}
|
||||
@ -254,7 +253,6 @@ export default async function ActivityPage({ searchParams }: PageProps) {
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
{filtered.length > 0 ? (
|
||||
|
||||
@ -99,7 +99,7 @@ export function ReminderFilterBar({ accounts }: FilterBarProps) {
|
||||
id="filter-account"
|
||||
value={initial.accountId}
|
||||
onChange={(e) => setParam("accountId", e.target.value)}
|
||||
className="h-8 w-full sm:max-w-xs rounded-lg border border-input bg-transparent px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="h-8 w-full rounded-lg border border-input bg-transparent px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
<option value="">All accounts</option>
|
||||
{accounts.map((a) => (
|
||||
|
||||
@ -187,11 +187,13 @@ export async function listActivityRuns(
|
||||
// exposes a stable shape. LEFT JOIN keeps orphan runs (whose reminder
|
||||
// has been deleted but history was preserved) in the list.
|
||||
// The `archived` flag flips the visibility filter:
|
||||
// false (default) — only non-archived rows
|
||||
// true — only archived rows (for the Archived tab)
|
||||
// false (default) — non-archived, non-skipped rows (skipped runs
|
||||
// belong to the Archived tab now)
|
||||
// true — archived rows OR skipped rows (they're treated
|
||||
// as "history" rather than active outcomes)
|
||||
const archivedClause = opts.archived
|
||||
? sql`rr.archived_at IS NOT NULL`
|
||||
: sql`rr.archived_at IS NULL`;
|
||||
? sql`(rr.archived_at IS NOT NULL OR rr.status = 'skipped')`
|
||||
: sql`rr.archived_at IS NULL AND rr.status <> 'skipped'`;
|
||||
const rows = await db.execute(sql`
|
||||
SELECT
|
||||
rr.id,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user