diff --git a/.server-changes/task-type-filter-segmented-control.md b/.server-changes/task-type-filter-segmented-control.md new file mode 100644 index 0000000000..a9f5d68c55 --- /dev/null +++ b/.server-changes/task-type-filter-segmented-control.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +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. diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index aea0238a03..39b5ed7aaf 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -35,15 +35,8 @@ import { collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { SearchInput } from "~/components/primitives/SearchInput"; -import { - ComboboxProvider, - SelectItem, - SelectList, - SelectPopover, - SelectProvider, - SelectTrigger, - shortcutFromIndex, -} from "~/components/primitives/Select"; +import SegmentedControl from "~/components/primitives/SegmentedControl"; +import { ShortcutKey } from "~/components/primitives/ShortcutKey"; import { Spinner } from "~/components/primitives/Spinner"; import { Table, @@ -69,6 +62,7 @@ import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { useShortcutKeys } from "~/hooks/useShortcutKeys"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { @@ -169,6 +163,25 @@ function parseTypesParam(values: string[]): UnifiedTaskKind[] { return values.filter((v): v is UnifiedTaskKind => VALID_KINDS.has(v as UnifiedTaskKind)); } +const ALL_TASK_TYPES = "ALL"; + +type TaskTypeSegment = typeof ALL_TASK_TYPES | UnifiedTaskKind; + +/** Segmented control options. "All" shows as a word; the task kinds show as + * icon-only segments. Every segment has a tooltip (label + shortcut). + * Order = shortcut keys 0–3. */ +const TASK_TYPE_SEGMENTS: { + value: TaskTypeSegment; + tooltip: string; + text?: string; + source?: UnifiedTaskKind; +}[] = [ + { value: ALL_TASK_TYPES, tooltip: "All tasks", text: "All" }, + { value: "AGENT", tooltip: "Agent tasks", source: "AGENT" }, + { value: "STANDARD", tooltip: "Standard tasks", source: "STANDARD" }, + { value: "SCHEDULED", tooltip: "Scheduled tasks", source: "SCHEDULED" }, +]; + const PAGE_SIZE = 25; export default function Page() { @@ -217,7 +230,8 @@ export default function Page() { const selectedTypes = useMemo(() => { const raw = parseTypesParam(values("types")); - return raw.length > 0 ? new Set(raw) : null; // null = all + // Single-select: one kind filters to it; none or legacy multi → all. + return raw.length === 1 ? new Set(raw) : null; // null = all }, [values]); const { filteredItems } = useFuzzyFilter({ @@ -265,7 +279,7 @@ export default function Page() {
- +
@@ -496,48 +510,80 @@ function RunningCell({ state }: { state: UnifiedRunningState | undefined }) { function TaskTypeFilter() { const { values, replace } = useSearchParams(); const raw = parseTypesParam(values("types")); - const isAll = raw.length === 0 || raw.length === KIND_OPTIONS.length; - // No filter → preselect everything so users can uncheck from "all". - const popoverValue = isAll ? KIND_OPTIONS.map((k) => k.value) : raw; - - const handleChange = (next: string[]) => { - // Empty or fully-selected → drop the param so the default (all) applies. Always reset page. - if (next.length === 0 || next.length === KIND_OPTIONS.length) { - replace({ types: undefined, page: undefined }); - } else { - replace({ types: next, page: undefined }); - } + // Single-select: exactly one kind selects it, anything else falls back to All. + const current: TaskTypeSegment = raw.length === 1 ? raw[0] : ALL_TASK_TYPES; + + const select = (value: string) => { + // "All" drops the param; a kind filters to just that kind. Always reset page. + replace({ types: value === ALL_TASK_TYPES ? undefined : [value], page: undefined }); }; - const label = isAll - ? "All" - : raw.map((v) => KIND_OPTIONS.find((k) => k.value === v)?.label ?? v).join(", "); + return ( + <> + {TASK_TYPE_SEGMENTS.map((option, index) => ( + select(option.value)} + /> + ))} + ({ + value: option.value, + label: , + }))} + /> + + ); +} + +// Registers a number-key shortcut that selects one segment. +function TaskTypeShortcut({ shortcut, onSelect }: { shortcut: string; onSelect: () => void }) { + useShortcutKeys({ + shortcut: { key: shortcut }, + action: (event) => { + event.preventDefault(); + onSelect(); + }, + }); + return null; +} +function TaskTypeSegmentLabel({ + option, + shortcut, +}: { + option: (typeof TASK_TYPE_SEGMENTS)[number]; + shortcut: string; +}) { return ( - - - - Task type: - {label} - - - - {KIND_OPTIONS.map((opt, index) => ( - - - - {opt.label} - - - ))} - - - - + + + {option.tooltip} + + ) : ( + {option.text} + ) + } + content={ +
+ {option.tooltip} + +
+ } + className="px-2 py-1.5 text-xs" + sideOffset={6} + disableHoverableContent + /> ); }