Skip to content

Commit ea9068f

Browse files
committed
feat(inbox): priority sorting, status filter, suggested reviewers
- Default sort: status → suggested reviewer → priority (P0 first) - Status multi-select filter replacing hardcoded constant - Priority as default sort option in toolbar - Blue badge on report cards for suggested reviewer - Search renamed to "Search reports...", disabled with tooltip when empty
1 parent 9e66fca commit ea9068f

File tree

4 files changed

+155
-28
lines changed

4 files changed

+155
-28
lines changed

apps/code/src/renderer/features/inbox/components/ReportCard.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import {
44
inboxStatusAccentCss,
55
inboxStatusLabel,
66
} from "@features/inbox/utils/inboxSort";
7-
import { Flex, Text } from "@radix-ui/themes";
7+
import { UserIcon } from "@phosphor-icons/react";
8+
import { Flex, Text, Tooltip } from "@radix-ui/themes";
89
import type { SignalReport } from "@shared/types";
910
import { motion } from "framer-motion";
1011
import type { KeyboardEvent, MouseEvent } from "react";
@@ -118,6 +119,20 @@ export function ReportCard({
118119
{statusLabel}
119120
</span>
120121
<SignalReportPriorityBadge priority={report.priority} />
122+
{report.is_suggested_reviewer && (
123+
<Tooltip content="You are a suggested reviewer">
124+
<span
125+
className="inline-flex shrink-0 items-center rounded-sm px-1 py-px"
126+
style={{
127+
color: "var(--blue-11)",
128+
backgroundColor: "var(--blue-3)",
129+
border: "1px solid var(--blue-6)",
130+
}}
131+
>
132+
<UserIcon size={10} weight="bold" />
133+
</span>
134+
</Tooltip>
135+
)}
121136
</Flex>
122137
</Flex>
123138
{/* Summary is outside the title row so wrapped lines align with title text (bullet + gap), not the card edge */}

apps/code/src/renderer/features/inbox/components/SignalsToolbar.tsx

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
2+
import {
3+
inboxStatusAccentCss,
4+
inboxStatusLabel,
5+
} from "@features/inbox/utils/inboxSort";
26
import {
37
CalendarPlus,
48
Check,
59
Clock,
610
FunnelSimple as FunnelSimpleIcon,
11+
ListNumbers,
712
MagnifyingGlass,
813
TrendUp,
914
} from "@phosphor-icons/react";
10-
import { Box, Flex, Popover, Text, TextField } from "@radix-ui/themes";
11-
import type { SignalReportOrderingField } from "@shared/types";
15+
import { Box, Flex, Popover, Text, TextField, Tooltip } from "@radix-ui/themes";
16+
import type {
17+
SignalReportOrderingField,
18+
SignalReportStatus,
19+
} from "@shared/types";
1220

1321
interface SignalsToolbarProps {
1422
totalCount: number;
@@ -17,16 +25,26 @@ interface SignalsToolbarProps {
1725
livePolling?: boolean;
1826
readyCount?: number;
1927
processingCount?: number;
28+
searchDisabledReason?: string | null;
2029
}
2130

2231
type SortOption = {
2332
label: string;
24-
field: Extract<SignalReportOrderingField, "created_at" | "total_weight">;
33+
field: Extract<
34+
SignalReportOrderingField,
35+
"priority" | "created_at" | "total_weight"
36+
>;
2537
direction: "asc" | "desc";
2638
icon: React.ReactNode;
2739
};
2840

2941
const sortOptions: SortOption[] = [
42+
{
43+
label: "Priority",
44+
field: "priority",
45+
direction: "asc",
46+
icon: <ListNumbers size={14} />,
47+
},
3048
{
3149
label: "Strongest signal",
3250
field: "total_weight",
@@ -47,19 +65,30 @@ const sortOptions: SortOption[] = [
4765
},
4866
];
4967

68+
const FILTERABLE_STATUSES: SignalReportStatus[] = [
69+
"ready",
70+
"pending_input",
71+
"in_progress",
72+
"candidate",
73+
"potential",
74+
];
75+
5076
export function SignalsToolbar({
5177
totalCount,
5278
filteredCount,
5379
isSearchActive,
5480
livePolling = false,
5581
readyCount,
5682
processingCount = 0,
83+
searchDisabledReason,
5784
}: SignalsToolbarProps) {
5885
const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery);
5986
const setSearchQuery = useInboxSignalsFilterStore((s) => s.setSearchQuery);
6087
const sortField = useInboxSignalsFilterStore((s) => s.sortField);
6188
const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection);
6289
const setSort = useInboxSignalsFilterStore((s) => s.setSort);
90+
const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter);
91+
const toggleStatus = useInboxSignalsFilterStore((s) => s.toggleStatus);
6392

6493
const countLabel = isSearchActive
6594
? `${filteredCount} of ${totalCount}`
@@ -103,38 +132,47 @@ export function SignalsToolbar({
103132
) : null}
104133
</Flex>
105134
</Flex>
106-
<SortMenu
135+
<FilterSortMenu
107136
sortField={sortField}
108137
sortDirection={sortDirection}
109138
onSort={setSort}
139+
statusFilter={statusFilter}
140+
onToggleStatus={toggleStatus}
110141
/>
111142
</Flex>
112-
<TextField.Root
113-
size="1"
114-
placeholder="Search signals..."
115-
value={searchQuery}
116-
onChange={(e) => setSearchQuery(e.target.value)}
117-
className="text-[12px]"
118-
>
119-
<TextField.Slot>
120-
<MagnifyingGlass size={12} />
121-
</TextField.Slot>
122-
</TextField.Root>
143+
<Tooltip content={searchDisabledReason} hidden={!searchDisabledReason}>
144+
<TextField.Root
145+
size="1"
146+
placeholder="Search reports..."
147+
value={searchQuery}
148+
onChange={(e) => setSearchQuery(e.target.value)}
149+
className="text-[12px]"
150+
disabled={!!searchDisabledReason}
151+
>
152+
<TextField.Slot>
153+
<MagnifyingGlass size={12} />
154+
</TextField.Slot>
155+
</TextField.Root>
156+
</Tooltip>
123157
</Flex>
124158
);
125159
}
126160

127-
function SortMenu({
161+
function FilterSortMenu({
128162
sortField,
129163
sortDirection,
130164
onSort,
165+
statusFilter,
166+
onToggleStatus,
131167
}: {
132168
sortField: string;
133169
sortDirection: string;
134170
onSort: (
135171
field: SortOption["field"],
136172
direction: SortOption["direction"],
137173
) => void;
174+
statusFilter: SignalReportStatus[];
175+
onToggleStatus: (status: SignalReportStatus) => void;
138176
}) {
139177
const itemClassName =
140178
"flex w-full items-center justify-between rounded-sm px-1 py-1 text-left text-[13px] text-gray-12 transition-colors hover:bg-gray-3";
@@ -144,7 +182,7 @@ function SortMenu({
144182
<Popover.Trigger>
145183
<button
146184
type="button"
147-
aria-label="Sort signals"
185+
aria-label="Filter and sort signals"
148186
className="flex h-6 w-6 items-center justify-center rounded-sm text-gray-10 transition-colors hover:bg-gray-3 hover:text-gray-12"
149187
>
150188
<FunnelSimpleIcon size={14} />
@@ -156,7 +194,7 @@ function SortMenu({
156194
sideOffset={6}
157195
style={{ padding: 8, minWidth: 220 }}
158196
>
159-
<Flex direction="column" gap="1">
197+
<Flex direction="column" gap="3">
160198
<Box>
161199
<Text
162200
size="1"
@@ -188,6 +226,42 @@ function SortMenu({
188226
})}
189227
</Box>
190228
</Box>
229+
230+
<Box>
231+
<Text
232+
size="1"
233+
className="text-gray-10"
234+
weight="medium"
235+
style={{ paddingLeft: "1px" }}
236+
>
237+
Status
238+
</Text>
239+
<Box mt="1">
240+
{FILTERABLE_STATUSES.map((status) => {
241+
const isActive = statusFilter.includes(status);
242+
const accent = inboxStatusAccentCss(status);
243+
return (
244+
<button
245+
key={status}
246+
type="button"
247+
className={itemClassName}
248+
onClick={() => onToggleStatus(status)}
249+
>
250+
<span className="flex items-center gap-1.5">
251+
<span
252+
className="inline-block h-2 w-2 shrink-0 rounded-full"
253+
style={{ backgroundColor: accent }}
254+
/>
255+
<span className="text-gray-12">
256+
{inboxStatusLabel(status)}
257+
</span>
258+
</span>
259+
{isActive && <Check size={12} className="text-gray-12" />}
260+
</button>
261+
);
262+
})}
263+
</Box>
264+
</Box>
191265
</Flex>
192266
</Popover.Content>
193267
</Popover.Root>

apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
1-
import type { SignalReportOrderingField } from "@shared/types";
1+
import type {
2+
SignalReportOrderingField,
3+
SignalReportStatus,
4+
} from "@shared/types";
25
import { create } from "zustand";
36
import { persist } from "zustand/middleware";
47

58
type SignalSortField = Extract<
69
SignalReportOrderingField,
7-
"created_at" | "total_weight"
10+
"priority" | "created_at" | "total_weight"
811
>;
912

1013
type SignalSortDirection = "asc" | "desc";
1114

15+
const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [
16+
"ready",
17+
"pending_input",
18+
"in_progress",
19+
"candidate",
20+
"potential",
21+
];
22+
1223
interface InboxSignalsFilterState {
1324
sortField: SignalSortField;
1425
sortDirection: SignalSortDirection;
1526
searchQuery: string;
27+
statusFilter: SignalReportStatus[];
1628
}
1729

1830
interface InboxSignalsFilterActions {
1931
setSort: (field: SignalSortField, direction: SignalSortDirection) => void;
2032
setSearchQuery: (query: string) => void;
33+
setStatusFilter: (statuses: SignalReportStatus[]) => void;
34+
toggleStatus: (status: SignalReportStatus) => void;
2135
}
2236

2337
type InboxSignalsFilterStore = InboxSignalsFilterState &
@@ -26,17 +40,28 @@ type InboxSignalsFilterStore = InboxSignalsFilterState &
2640
export const useInboxSignalsFilterStore = create<InboxSignalsFilterStore>()(
2741
persist(
2842
(set) => ({
29-
sortField: "total_weight",
30-
sortDirection: "desc",
43+
sortField: "priority",
44+
sortDirection: "asc",
3145
searchQuery: "",
46+
statusFilter: DEFAULT_STATUS_FILTER,
3247
setSort: (sortField, sortDirection) => set({ sortField, sortDirection }),
3348
setSearchQuery: (searchQuery) => set({ searchQuery }),
49+
setStatusFilter: (statusFilter) => set({ statusFilter }),
50+
toggleStatus: (status) =>
51+
set((state) => {
52+
const current = state.statusFilter;
53+
const next = current.includes(status)
54+
? current.filter((s) => s !== status)
55+
: [...current, status];
56+
return { statusFilter: next.length > 0 ? next : current };
57+
}),
3458
}),
3559
{
3660
name: "inbox-signals-filter-storage",
3761
partialize: (state) => ({
3862
sortField: state.sortField,
3963
sortDirection: state.sortDirection,
64+
statusFilter: state.statusFilter,
4065
}),
4166
},
4267
),

apps/code/src/renderer/features/inbox/utils/filterReports.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { SignalReport, SignalReportOrderingField } from "@shared/types";
1+
import type {
2+
SignalReport,
3+
SignalReportOrderingField,
4+
SignalReportStatus,
5+
} from "@shared/types";
26

37
export function filterReportsBySearch(
48
reports: SignalReport[],
@@ -16,13 +20,22 @@ export function filterReportsBySearch(
1620
}
1721

1822
/**
19-
* Comma-separated `ordering` for the signal report list API: semantic `status` rank
20-
* then the toolbar field (matches default inbox UX).
23+
* Build a comma-separated status filter string for the API from an array of statuses.
24+
*/
25+
export function buildStatusFilterParam(statuses: SignalReportStatus[]): string {
26+
return statuses.join(",");
27+
}
28+
29+
/**
30+
* Comma-separated `ordering` for the signal report list API:
31+
* 1. Status rank (ready first — semantic server-side rank, always applied)
32+
* 2. Suggested reviewer (current user's reports first)
33+
* 3. Toolbar-selected field (priority, total_weight, created_at, etc.)
2134
*/
2235
export function buildSignalReportListOrdering(
2336
field: SignalReportOrderingField,
2437
direction: "asc" | "desc",
2538
): string {
26-
const secondary = direction === "desc" ? `-${field}` : field;
27-
return `status,${secondary}`;
39+
const fieldKey = direction === "desc" ? `-${field}` : field;
40+
return `status,-is_suggested_reviewer,${fieldKey}`;
2841
}

0 commit comments

Comments
 (0)