Skip to content

Commit 5c62af0

Browse files
committed
feat(webapp): segmented control for the task type filter
Replace the popover task type filter on the Tasks page with a segmented control: "All" plus icon-only Agent, Standard, and Scheduled segments, each with a tooltip showing its label and number-key shortcut (0-3). Filtering is now single-select instead of multi-select.
1 parent ae08c9c commit 5c62af0

2 files changed

Lines changed: 101 additions & 49 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Replace the Task type filter on the Tasks page with a segmented control: "All" plus icon-only Agent, Standard, and Scheduled segments (each with a tooltip showing its label and number-key shortcut). Filtering is now single-select (one task type at a time) instead of multi-select. Shortcut keys 0–3 select each segment.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx

Lines changed: 95 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,8 @@ import {
3535
collapsibleHandleClassName,
3636
} from "~/components/primitives/Resizable";
3737
import { SearchInput } from "~/components/primitives/SearchInput";
38-
import {
39-
ComboboxProvider,
40-
SelectItem,
41-
SelectList,
42-
SelectPopover,
43-
SelectProvider,
44-
SelectTrigger,
45-
shortcutFromIndex,
46-
} from "~/components/primitives/Select";
38+
import SegmentedControl from "~/components/primitives/SegmentedControl";
39+
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
4740
import { Spinner } from "~/components/primitives/Spinner";
4841
import {
4942
Table,
@@ -69,6 +62,7 @@ import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
6962
import { useOrganization } from "~/hooks/useOrganizations";
7063
import { useProject } from "~/hooks/useProject";
7164
import { useSearchParams } from "~/hooks/useSearchParam";
65+
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
7266
import { findProjectBySlug } from "~/models/project.server";
7367
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
7468
import {
@@ -169,6 +163,25 @@ function parseTypesParam(values: string[]): UnifiedTaskKind[] {
169163
return values.filter((v): v is UnifiedTaskKind => VALID_KINDS.has(v as UnifiedTaskKind));
170164
}
171165

166+
const ALL_TASK_TYPES = "ALL";
167+
168+
type TaskTypeSegment = typeof ALL_TASK_TYPES | UnifiedTaskKind;
169+
170+
/** Segmented control options. "All" shows as a word; the task kinds show as
171+
* icon-only segments. Every segment has a tooltip (label + shortcut).
172+
* Order = shortcut keys 0–3. */
173+
const TASK_TYPE_SEGMENTS: {
174+
value: TaskTypeSegment;
175+
tooltip: string;
176+
text?: string;
177+
source?: UnifiedTaskKind;
178+
}[] = [
179+
{ value: ALL_TASK_TYPES, tooltip: "All tasks", text: "All" },
180+
{ value: "AGENT", tooltip: "Agent tasks", source: "AGENT" },
181+
{ value: "STANDARD", tooltip: "Standard tasks", source: "STANDARD" },
182+
{ value: "SCHEDULED", tooltip: "Scheduled tasks", source: "SCHEDULED" },
183+
];
184+
172185
const PAGE_SIZE = 25;
173186

174187
export default function Page() {
@@ -217,7 +230,8 @@ export default function Page() {
217230

218231
const selectedTypes = useMemo(() => {
219232
const raw = parseTypesParam(values("types"));
220-
return raw.length > 0 ? new Set(raw) : null; // null = all
233+
// Single-select: one kind filters to it; none or legacy multi → all.
234+
return raw.length === 1 ? new Set(raw) : null; // null = all
221235
}, [values]);
222236

223237
const { filteredItems } = useFuzzyFilter<UnifiedTaskListItem>({
@@ -265,7 +279,7 @@ export default function Page() {
265279
<div className="flex h-full flex-col overflow-hidden">
266280
<div className="flex shrink-0 items-center justify-between gap-1.5 p-2">
267281
<div className="flex flex-1 items-center gap-1.5">
268-
<SearchInput placeholder="Search tasks…" autoFocus resetParams={["page"]} />
282+
<SearchInput placeholder="Search tasks…" resetParams={["page"]} />
269283
<TaskTypeFilter />
270284
</div>
271285
<div className="flex items-center gap-1.5">
@@ -496,48 +510,80 @@ function RunningCell({ state }: { state: UnifiedRunningState | undefined }) {
496510
function TaskTypeFilter() {
497511
const { values, replace } = useSearchParams();
498512
const raw = parseTypesParam(values("types"));
499-
const isAll = raw.length === 0 || raw.length === KIND_OPTIONS.length;
500-
// No filter → preselect everything so users can uncheck from "all".
501-
const popoverValue = isAll ? KIND_OPTIONS.map((k) => k.value) : raw;
502-
503-
const handleChange = (next: string[]) => {
504-
// Empty or fully-selected → drop the param so the default (all) applies. Always reset page.
505-
if (next.length === 0 || next.length === KIND_OPTIONS.length) {
506-
replace({ types: undefined, page: undefined });
507-
} else {
508-
replace({ types: next, page: undefined });
509-
}
513+
// Single-select: exactly one kind selects it, anything else falls back to All.
514+
const current: TaskTypeSegment = raw.length === 1 ? raw[0] : ALL_TASK_TYPES;
515+
516+
const select = (value: string) => {
517+
// "All" drops the param; a kind filters to just that kind. Always reset page.
518+
replace({ types: value === ALL_TASK_TYPES ? undefined : [value], page: undefined });
510519
};
511520

512-
const label = isAll
513-
? "All"
514-
: raw.map((v) => KIND_OPTIONS.find((k) => k.value === v)?.label ?? v).join(", ");
521+
return (
522+
<>
523+
{TASK_TYPE_SEGMENTS.map((option, index) => (
524+
<TaskTypeShortcut
525+
key={option.value}
526+
shortcut={String(index)}
527+
onSelect={() => select(option.value)}
528+
/>
529+
))}
530+
<SegmentedControl
531+
name="task-type"
532+
value={current}
533+
variant="secondary/small"
534+
onChange={select}
535+
options={TASK_TYPE_SEGMENTS.map((option, index) => ({
536+
value: option.value,
537+
label: <TaskTypeSegmentLabel option={option} shortcut={String(index)} />,
538+
}))}
539+
/>
540+
</>
541+
);
542+
}
543+
544+
// Registers a number-key shortcut that selects one segment.
545+
function TaskTypeShortcut({ shortcut, onSelect }: { shortcut: string; onSelect: () => void }) {
546+
useShortcutKeys({
547+
shortcut: { key: shortcut },
548+
action: (event) => {
549+
event.preventDefault();
550+
onSelect();
551+
},
552+
});
553+
return null;
554+
}
515555

556+
function TaskTypeSegmentLabel({
557+
option,
558+
shortcut,
559+
}: {
560+
option: (typeof TASK_TYPE_SEGMENTS)[number];
561+
shortcut: string;
562+
}) {
516563
return (
517-
<ComboboxProvider>
518-
<SelectProvider value={popoverValue} setValue={handleChange} virtualFocus>
519-
<SelectTrigger variant="secondary/small" dropdownIcon>
520-
<span className="text-text-bright">Task type: </span>
521-
<span className="max-w-[180px] truncate text-text-dimmed">{label}</span>
522-
</SelectTrigger>
523-
<SelectPopover className="min-w-fit">
524-
<SelectList>
525-
{KIND_OPTIONS.map((opt, index) => (
526-
<SelectItem
527-
key={opt.value}
528-
value={opt.value}
529-
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
530-
>
531-
<span className="flex items-center gap-2">
532-
<TaskTriggerSourceIcon source={opt.value} />
533-
<span className="text-text-bright">{opt.label}</span>
534-
</span>
535-
</SelectItem>
536-
))}
537-
</SelectList>
538-
</SelectPopover>
539-
</SelectProvider>
540-
</ComboboxProvider>
564+
<SimpleTooltip
565+
asChild
566+
button={
567+
option.source ? (
568+
// -mx-0.5 tightens the icon segment toward a square button.
569+
<span className="-mx-0.5 flex items-center justify-center">
570+
<TaskTriggerSourceIcon source={option.source} />
571+
<span className="sr-only">{option.tooltip}</span>
572+
</span>
573+
) : (
574+
<span className="flex items-center justify-center">{option.text}</span>
575+
)
576+
}
577+
content={
578+
<div className="flex items-center gap-1">
579+
<span className="text-text-bright">{option.tooltip}</span>
580+
<ShortcutKey shortcut={{ key: shortcut }} variant="small" />
581+
</div>
582+
}
583+
className="px-2 py-1.5 text-xs"
584+
sideOffset={6}
585+
disableHoverableContent
586+
/>
541587
);
542588
}
543589

0 commit comments

Comments
 (0)