Skip to content
Closed
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
17 changes: 16 additions & 1 deletion apps/code/src/renderer/features/inbox/components/ReportCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
inboxStatusAccentCss,
inboxStatusLabel,
} from "@features/inbox/utils/inboxSort";
import { Flex, Text } from "@radix-ui/themes";
import { UserIcon } from "@phosphor-icons/react";
import { Flex, Text, Tooltip } from "@radix-ui/themes";
import type { SignalReport } from "@shared/types";
import { motion } from "framer-motion";
import type { KeyboardEvent, MouseEvent } from "react";
Expand Down Expand Up @@ -118,6 +119,20 @@ export function ReportCard({
{statusLabel}
</span>
<SignalReportPriorityBadge priority={report.priority} />
{report.is_suggested_reviewer && (
<Tooltip content="You are a suggested reviewer">
<span
className="inline-flex shrink-0 items-center rounded-sm px-1 py-px"
style={{
color: "var(--blue-11)",
backgroundColor: "var(--blue-3)",
border: "1px solid var(--blue-6)",
}}
>
<UserIcon size={10} weight="bold" />
</span>
</Tooltip>
)}
</Flex>
</Flex>
{/* Summary is outside the title row so wrapped lines align with title text (bullet + gap), not the card edge */}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore";
import {
inboxStatusAccentCss,
inboxStatusLabel,
} from "@features/inbox/utils/inboxSort";
import {
CalendarPlus,
Check,
Clock,
FunnelSimple as FunnelSimpleIcon,
ListNumbers,
MagnifyingGlass,
TrendUp,
} from "@phosphor-icons/react";
import { Box, Flex, Popover, Text, TextField } from "@radix-ui/themes";
import type { SignalReportOrderingField } from "@shared/types";
import type {
SignalReportOrderingField,
SignalReportStatus,
} from "@shared/types";

interface SignalsToolbarProps {
totalCount: number;
Expand All @@ -21,12 +29,21 @@ interface SignalsToolbarProps {

type SortOption = {
label: string;
field: Extract<SignalReportOrderingField, "created_at" | "total_weight">;
field: Extract<
SignalReportOrderingField,
"priority" | "created_at" | "total_weight"
>;
direction: "asc" | "desc";
icon: React.ReactNode;
};

const sortOptions: SortOption[] = [
{
label: "Priority",
field: "priority",
direction: "asc",
icon: <ListNumbers size={14} />,
},
{
label: "Strongest signal",
field: "total_weight",
Expand All @@ -47,6 +64,14 @@ const sortOptions: SortOption[] = [
},
];

const FILTERABLE_STATUSES: SignalReportStatus[] = [
"ready",
"pending_input",
"in_progress",
"candidate",
"potential",
];

export function SignalsToolbar({
totalCount,
filteredCount,
Expand All @@ -60,6 +85,8 @@ export function SignalsToolbar({
const sortField = useInboxSignalsFilterStore((s) => s.sortField);
const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection);
const setSort = useInboxSignalsFilterStore((s) => s.setSort);
const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter);
const toggleStatus = useInboxSignalsFilterStore((s) => s.toggleStatus);

const countLabel = isSearchActive
? `${filteredCount} of ${totalCount}`
Expand Down Expand Up @@ -103,10 +130,12 @@ export function SignalsToolbar({
) : null}
</Flex>
</Flex>
<SortMenu
<FilterSortMenu
sortField={sortField}
sortDirection={sortDirection}
onSort={setSort}
statusFilter={statusFilter}
onToggleStatus={toggleStatus}
/>
</Flex>
<TextField.Root
Expand All @@ -124,17 +153,21 @@ export function SignalsToolbar({
);
}

function SortMenu({
function FilterSortMenu({
sortField,
sortDirection,
onSort,
statusFilter,
onToggleStatus,
}: {
sortField: string;
sortDirection: string;
onSort: (
field: SortOption["field"],
direction: SortOption["direction"],
) => void;
statusFilter: SignalReportStatus[];
onToggleStatus: (status: SignalReportStatus) => void;
}) {
const itemClassName =
"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";
Expand All @@ -144,7 +177,7 @@ function SortMenu({
<Popover.Trigger>
<button
type="button"
aria-label="Sort signals"
aria-label="Filter and sort signals"
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"
>
<FunnelSimpleIcon size={14} />
Expand All @@ -156,7 +189,7 @@ function SortMenu({
sideOffset={6}
style={{ padding: 8, minWidth: 220 }}
>
<Flex direction="column" gap="1">
<Flex direction="column" gap="3">
<Box>
<Text
size="1"
Expand Down Expand Up @@ -188,6 +221,42 @@ function SortMenu({
})}
</Box>
</Box>

<Box>
<Text
size="1"
className="text-gray-10"
weight="medium"
style={{ paddingLeft: "1px" }}
>
Status
</Text>
<Box mt="1">
{FILTERABLE_STATUSES.map((status) => {
const isActive = statusFilter.includes(status);
const accent = inboxStatusAccentCss(status);
return (
<button
key={status}
type="button"
className={itemClassName}
onClick={() => onToggleStatus(status)}
>
<span className="flex items-center gap-1.5">
<span
className="inline-block h-2 w-2 shrink-0 rounded-full"
style={{ backgroundColor: accent }}
/>
<span className="text-gray-12">
{inboxStatusLabel(status)}
</span>
</span>
{isActive && <Check size={12} className="text-gray-12" />}
</button>
);
})}
</Box>
</Box>
</Flex>
</Popover.Content>
</Popover.Root>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import type { SignalReportOrderingField } from "@shared/types";
import type {
SignalReportOrderingField,
SignalReportStatus,
} from "@shared/types";
import { create } from "zustand";
import { persist } from "zustand/middleware";

type SignalSortField = Extract<
SignalReportOrderingField,
"created_at" | "total_weight"
"priority" | "created_at" | "total_weight"
>;

type SignalSortDirection = "asc" | "desc";

const DEFAULT_STATUS_FILTER: SignalReportStatus[] = [
"ready",
"pending_input",
"in_progress",
"candidate",
"potential",
];

interface InboxSignalsFilterState {
sortField: SignalSortField;
sortDirection: SignalSortDirection;
searchQuery: string;
statusFilter: SignalReportStatus[];
}

interface InboxSignalsFilterActions {
setSort: (field: SignalSortField, direction: SignalSortDirection) => void;
setSearchQuery: (query: string) => void;
setStatusFilter: (statuses: SignalReportStatus[]) => void;
toggleStatus: (status: SignalReportStatus) => void;
}

type InboxSignalsFilterStore = InboxSignalsFilterState &
Expand All @@ -26,17 +40,28 @@ type InboxSignalsFilterStore = InboxSignalsFilterState &
export const useInboxSignalsFilterStore = create<InboxSignalsFilterStore>()(
persist(
(set) => ({
sortField: "total_weight",
sortDirection: "desc",
sortField: "priority",
sortDirection: "asc",
searchQuery: "",
statusFilter: DEFAULT_STATUS_FILTER,
setSort: (sortField, sortDirection) => set({ sortField, sortDirection }),
setSearchQuery: (searchQuery) => set({ searchQuery }),
setStatusFilter: (statusFilter) => set({ statusFilter }),
toggleStatus: (status) =>
set((state) => {
const current = state.statusFilter;
const next = current.includes(status)
? current.filter((s) => s !== status)
: [...current, status];
return { statusFilter: next.length > 0 ? next : current };
}),
}),
{
name: "inbox-signals-filter-storage",
partialize: (state) => ({
sortField: state.sortField,
sortDirection: state.sortDirection,
statusFilter: state.statusFilter,
}),
},
),
Expand Down
23 changes: 18 additions & 5 deletions apps/code/src/renderer/features/inbox/utils/filterReports.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { SignalReport, SignalReportOrderingField } from "@shared/types";
import type {
SignalReport,
SignalReportOrderingField,
SignalReportStatus,
} from "@shared/types";

export function filterReportsBySearch(
reports: SignalReport[],
Expand All @@ -16,13 +20,22 @@ export function filterReportsBySearch(
}

/**
* Comma-separated `ordering` for the signal report list API: semantic `status` rank
* then the toolbar field (matches default inbox UX).
* Build a comma-separated status filter string for the API from an array of statuses.
*/
export function buildStatusFilterParam(statuses: SignalReportStatus[]): string {
return statuses.join(",");
}

/**
* Comma-separated `ordering` for the signal report list API:
* 1. Status rank (ready first — semantic server-side rank, always applied)
* 2. Suggested reviewer (current user's reports first)
* 3. Toolbar-selected field (priority, total_weight, created_at, etc.)
*/
export function buildSignalReportListOrdering(
field: SignalReportOrderingField,
direction: "asc" | "desc",
): string {
const secondary = direction === "desc" ? `-${field}` : field;
return `status,${secondary}`;
const fieldKey = direction === "desc" ? `-${field}` : field;
return `status,-is_suggested_reviewer,${fieldKey}`;
}
Loading