diff --git a/ui/src/components/RoutineList.tsx b/ui/src/components/RoutineList.tsx index a4abe4cb9c6..5c19bf524e0 100644 --- a/ui/src/components/RoutineList.tsx +++ b/ui/src/components/RoutineList.tsx @@ -63,6 +63,7 @@ export function RoutineListRow({ disableRunNow = false, disableToggle = false, hideArchiveAction = false, + divider = true, onRunNow, onToggleEnabled, onToggleArchived, @@ -80,6 +81,8 @@ export function RoutineListRow({ disableRunNow?: boolean; disableToggle?: boolean; hideArchiveAction?: boolean; + /** Render a bottom divider between consecutive rows. Off when the group is its own card. */ + divider?: boolean; onRunNow: (routine: TRoutine) => void; onToggleEnabled: (routine: TRoutine, enabled: boolean) => void; onToggleArchived?: (routine: TRoutine) => void; @@ -95,7 +98,9 @@ export function RoutineListRow({ return (
diff --git a/ui/src/pages/Routines.tsx b/ui/src/pages/Routines.tsx index 0139bf45b97..ad1822f83bb 100644 --- a/ui/src/pages/Routines.tsx +++ b/ui/src/pages/Routines.tsx @@ -885,51 +885,60 @@ export function Routines() { />
) : ( -
- {routineGroups.map((group) => ( - { - updateRoutineView({ - collapsedGroups: open - ? routineViewState.collapsedGroups.filter((item) => item !== group.key) - : [...routineViewState.collapsedGroups, group.key], - }); - }} - > - {group.label ? ( -
- - - - {group.label} +
+ {routineGroups.map((group) => { + const isOpen = !routineViewState.collapsedGroups.includes(group.key); + return ( + { + updateRoutineView({ + collapsedGroups: open + ? routineViewState.collapsedGroups.filter((item) => item !== group.key) + : [...routineViewState.collapsedGroups, group.key], + }); + }} + className="overflow-hidden rounded-lg border border-border" + > + {group.label ? ( +
+ + + + {group.label} + + + + {group.items.length} - - - {group.items.length} - -
- ) : null} - - {group.items.map((routine) => ( - - ))} - -
- ))} +
+ ) : null} + + {group.items.map((routine) => ( + + ))} + + + ); + })}
)}
diff --git a/ui/storybook/stories/routines-list-groups.stories.tsx b/ui/storybook/stories/routines-list-groups.stories.tsx new file mode 100644 index 00000000000..d0aac2ba741 --- /dev/null +++ b/ui/storybook/stories/routines-list-groups.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useState } from "react"; +import { ChevronRight } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { + RoutineListRow, + type RoutineListAgentSummary, + type RoutineListProjectSummary, + type RoutineListRowItem, +} from "@/components/RoutineList"; + +const projectById = new Map([ + ["p1", { name: "Board UI", color: "#6366f1" }], + ["p2", { name: "Growth", color: "#10b981" }], +]); +const agentById = new Map([ + ["a1", { name: "CodexCoder", icon: null }], + ["a2", { name: "Digest Bot", icon: null }], +]); + +type Group = { key: string; label: string | null; items: RoutineListRowItem[] }; + +const GROUPS: Group[] = [ + { + key: "p1", + label: "Board UI", + items: [ + { id: "r1", title: "Weekly digest", status: "active", projectId: "p1", assigneeAgentId: "a2", lastRun: { triggeredAt: "2026-06-09T08:00:00Z", status: "succeeded" } }, + { id: "r2", title: "Triage stale issues", status: "active", projectId: "p1", assigneeAgentId: "a1", lastRun: { triggeredAt: "2026-06-08T08:00:00Z", status: "succeeded" } }, + { id: "r3", title: "Nightly changelog draft", status: "paused", projectId: "p1", assigneeAgentId: "a1", lastRun: null }, + ], + }, + { + key: "p2", + label: "Growth", + items: [ + { id: "r4", title: "Lead enrichment sweep", status: "active", projectId: "p2", assigneeAgentId: "a1", lastRun: { triggeredAt: "2026-06-09T06:00:00Z", status: "running" } }, + { id: "r5", title: "Outbound follow-ups", status: "active", projectId: "p2", assigneeAgentId: "a2", lastRun: { triggeredAt: "2026-06-07T06:00:00Z", status: "failed" } }, + ], + }, +]; + +function GroupedList() { + const [collapsed, setCollapsed] = useState([]); + return ( +
+
+ {GROUPS.map((group) => { + const isOpen = !collapsed.includes(group.key); + return ( + + setCollapsed((prev) => (open ? prev.filter((k) => k !== group.key) : [...prev, group.key])) + } + className="overflow-hidden rounded-lg border border-border" + > + {group.label ? ( +
+ + + {group.label} + + {group.items.length} +
+ ) : null} + + {group.items.map((routine) => ( + {}} + onToggleEnabled={() => {}} + onToggleArchived={() => {}} + /> + ))} + +
+ ); + })} +
+
+ ); +} + +const meta: Meta = { + title: "Product/Routines ยท List (grouped cards)", + component: GroupedList, + parameters: { layout: "fullscreen" }, +}; +export default meta; + +type Story = StoryObj; + +export const GroupedCards: Story = {};