feat: customizable Quick Actions on the Overview dashboard (#557)

* feat(panel): customizable Quick Actions on Overview — registry, defaults, persisted picker

* fix(panel): legacy bar actions join the quick-action defaults; empty state; persist-contract test

* docs(map): panel entries for this wave

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-18 07:05:20 +02:00
committed by GitHub
co-authored by Renn F
parent 02601fef22
commit 1a726d4c34
12 changed files with 708 additions and 71 deletions
@@ -55,8 +55,8 @@ vi.mock("../active-blockers-panel", () => ({
vi.mock("../recent-activity-feed", () => ({
RecentActivityFeed: () => <div>RecentActivityFeedStub</div>,
}));
vi.mock("../quick-actions-bar", () => ({
QuickActionsBar: () => <div>QuickActionsBarStub</div>,
vi.mock("../quick-actions-card", () => ({
QuickActionsCard: () => <div>QuickActionsCardStub</div>,
}));
vi.mock("../ceo-approval-queue", () => ({
CeoApprovalQueue: () => <div>CeoApprovalQueueStub</div>,
@@ -111,7 +111,7 @@ describe("CommandCenter", () => {
it("renders every section", () => {
render(<CommandCenter />);
for (const stub of [
"QuickActionsBarStub",
"QuickActionsCardStub",
"KeyMetricsPanelStub",
"AuditorAlertsPanelStub",
"UsageOverviewPanelStub",
@@ -135,7 +135,7 @@ describe("CommandCenter", () => {
it("orders sections top to bottom: quick actions/key cards, team health, then the rest", () => {
render(<CommandCenter />);
const order = [
"QuickActionsBarStub",
"QuickActionsCardStub",
"KeyMetricsPanelStub",
"TeamHealthCardsStub",
"CeoApprovalQueueStub",
@@ -0,0 +1,148 @@
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useUIStore } from "@/store";
import { DEFAULT_QUICK_ACTION_IDS } from "../quick-actions-registry";
import { QuickActionsCard } from "../quick-actions-card";
function resetStore(quickActionIds: string[] = DEFAULT_QUICK_ACTION_IDS) {
useUIStore.setState({ quickActionIds });
}
describe("QuickActionsCard", () => {
beforeEach(() => {
resetStore();
});
it("renders the default action set as links to their real routes on a fresh store", () => {
render(<QuickActionsCard />);
expect(screen.getByRole("link", { name: /New Task/i })).toHaveAttribute(
"href",
"/prompter",
);
expect(screen.getByRole("link", { name: /^Tasks$/i })).toHaveAttribute(
"href",
"/tasks",
);
expect(screen.getByRole("link", { name: /Settings/i })).toHaveAttribute(
"href",
"/settings",
);
});
it("drops a stale id (removed from the registry) without crashing", () => {
resetStore(["tasks", "this-action-no-longer-exists", "settings"]);
render(<QuickActionsCard />);
expect(screen.getByRole("link", { name: /^Tasks$/i })).toBeInTheDocument();
expect(screen.getByRole("link", { name: /Settings/i })).toBeInTheDocument();
expect(screen.getAllByRole("link")).toHaveLength(2);
});
it("renders nothing but the customize button when the stored list is entirely stale", () => {
resetStore(["ghost-one", "ghost-two"]);
render(<QuickActionsCard />);
expect(screen.queryAllByRole("link")).toHaveLength(0);
expect(
screen.getByRole("button", { name: "Customize Quick Actions" }),
).toBeInTheDocument();
});
describe("customize dialog", () => {
async function openDialog(user: ReturnType<typeof userEvent.setup>) {
render(<QuickActionsCard />);
await user.click(
screen.getByRole("button", { name: "Customize Quick Actions" }),
);
return screen.getByRole("dialog");
}
it("unchecking an action removes it from the card and persists to the store", async () => {
const user = userEvent.setup();
const dialog = await openDialog(user);
await user.click(
within(dialog).getByRole("checkbox", { name: "Show Tasks" }),
);
expect(useUIStore.getState().quickActionIds).not.toContain("tasks");
});
it("checking an action not in the default set adds it", async () => {
const user = userEvent.setup();
const dialog = await openDialog(user);
// Social isn't in DEFAULT_QUICK_ACTION_IDS (auditor now is — the
// legacy bar's actions all ride the defaults).
await user.click(
within(dialog).getByRole("checkbox", { name: "Show Social" }),
);
expect(useUIStore.getState().quickActionIds).toContain("social");
});
it("reorders with the move-later arrow", async () => {
const user = userEvent.setup();
const dialog = await openDialog(user);
const before = useUIStore.getState().quickActionIds;
expect(before[0]).toBe("new-task");
await user.click(
within(dialog).getByRole("button", { name: "Move New Task later" }),
);
const after = useUIStore.getState().quickActionIds;
expect(after[1]).toBe("new-task");
expect(after[0]).toBe(before[1]);
});
it("the move-earlier arrow is disabled for the first row", async () => {
const user = userEvent.setup();
const dialog = await openDialog(user);
expect(
within(dialog).getByRole("button", { name: "Move New Task earlier" }),
).toBeDisabled();
});
it("resets to defaults", async () => {
const user = userEvent.setup();
const dialog = await openDialog(user);
await user.click(
within(dialog).getByRole("checkbox", { name: "Show Tasks" }),
);
expect(useUIStore.getState().quickActionIds).not.toEqual(
DEFAULT_QUICK_ACTION_IDS,
);
await user.click(
within(dialog).getByRole("button", { name: "Reset to defaults" }),
);
expect(useUIStore.getState().quickActionIds).toEqual(
DEFAULT_QUICK_ACTION_IDS,
);
});
it("drops a stale id from the checklist instead of rendering it", async () => {
const user = userEvent.setup();
resetStore(["tasks", "ghost-stale"]);
render(<QuickActionsCard />);
await user.click(
screen.getByRole("button", { name: "Customize Quick Actions" }),
);
const dialog = screen.getByRole("dialog");
expect(within(dialog).queryByText("ghost-stale")).not.toBeInTheDocument();
expect(
within(dialog).getByRole("checkbox", { name: "Show Tasks" }),
).toBeInTheDocument();
});
});
});
@@ -0,0 +1,48 @@
import { describe, it, expect } from "vitest";
import {
QUICK_ACTIONS_REGISTRY,
DEFAULT_QUICK_ACTION_IDS,
resolveQuickActions,
isKnownQuickActionId,
} from "../quick-actions-registry";
describe("quick-actions-registry", () => {
it("has unique ids", () => {
const ids = QUICK_ACTIONS_REGISTRY.map((a) => a.id);
expect(new Set(ids).size).toBe(ids.length);
});
it("every action has a non-empty, absolute href", () => {
for (const action of QUICK_ACTIONS_REGISTRY) {
expect(action.href).toBeTruthy();
expect(action.href.startsWith("/")).toBe(true);
}
});
it("every action has a non-empty label, tip, and icon", () => {
for (const action of QUICK_ACTIONS_REGISTRY) {
expect(action.label).toBeTruthy();
expect(action.tip).toBeTruthy();
expect(action.icon).toBeTruthy();
}
});
it("default ids all resolve to real registry entries", () => {
for (const id of DEFAULT_QUICK_ACTION_IDS) {
expect(isKnownQuickActionId(id)).toBe(true);
}
});
it("resolveQuickActions preserves order and drops unknown ids", () => {
const resolved = resolveQuickActions([
"settings",
"does-not-exist",
"tasks",
]);
expect(resolved.map((a) => a.id)).toEqual(["settings", "tasks"]);
});
it("resolveQuickActions returns an empty list for an all-stale input", () => {
expect(resolveQuickActions(["ghost-1", "ghost-2"])).toEqual([]);
});
});
@@ -14,7 +14,7 @@ import { KeyMetricsPanel } from "./key-metrics-panel";
import { AuditorAlertsPanel } from "./auditor-alerts-panel";
import { ActiveBlockersPanel } from "./active-blockers-panel";
import { RecentActivityFeed } from "./recent-activity-feed";
import { QuickActionsBar } from "./quick-actions-bar";
import { QuickActionsCard } from "./quick-actions-card";
import { CeoApprovalQueue } from "./ceo-approval-queue";
import { PrReviewQueue } from "./pr-review-queue";
import { ReleaseProposalCard } from "./release-proposal-card";
@@ -154,7 +154,7 @@ export function CommandCenter() {
Quick Actions
</h2>
</HelpTip>
<QuickActionsBar />
<QuickActionsCard />
</section>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-4">
+1 -1
View File
@@ -7,7 +7,7 @@ export { AuditorAlertsPanel } from "./auditor-alerts-panel";
export { ActiveBlockersPanel } from "./active-blockers-panel";
export { RecentActivityFeed } from "./recent-activity-feed";
export { ActivityItem } from "./activity-item";
export { QuickActionsBar } from "./quick-actions-bar";
export { QuickActionsCard } from "./quick-actions-card";
export { HealthIndicator } from "./health-indicator";
export { CeoApprovalQueue } from "./ceo-approval-queue";
export { ReleaseProposalCard } from "./release-proposal-card";
@@ -1,60 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { CreateTaskDialog } from "@/components/tasks/create-task-dialog";
import { HelpTip } from "@/components/ui/help-tip";
import { Users, BookOpen, Shield, Sparkles, Bot } from "lucide-react";
import Link from "next/link";
export function QuickActionsBar() {
return (
<div className="flex flex-wrap gap-3">
<CreateTaskDialog />
<Link href="/agents" prefetch={false}>
<HelpTip label="Agents page — view the roster and spawn a new agent run">
<Button variant="outline">
<Users className="h-4 w-4 mr-2" />
Spawn Agent
</Button>
</HelpTip>
</Link>
<Link href="/prompter" prefetch={false}>
<HelpTip label="Chat-based interview to draft and submit a new task">
<Button variant="outline">
<Sparkles className="h-4 w-4 mr-2" />
Task Intake
</Button>
</HelpTip>
</Link>
<Link href="/business?tab=secretary" prefetch={false}>
<HelpTip label="Chat with the Secretary — company state and gated CEO directives">
<Button variant="outline">
<Bot className="h-4 w-4 mr-2" />
Secretary
</Button>
</HelpTip>
</Link>
<Link href="/journals" prefetch={false}>
<HelpTip label="Browse agent journal entries and learnings">
<Button variant="outline">
<BookOpen className="h-4 w-4 mr-2" />
View Journals
</Button>
</HelpTip>
</Link>
<Link href="/auditor" prefetch={false}>
<HelpTip label="The Auditor's flagged issues and reports">
<Button variant="outline">
<Shield className="h-4 w-4 mr-2" />
Auditor Report
</Button>
</HelpTip>
</Link>
</div>
);
}
@@ -0,0 +1,214 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { HelpTip } from "@/components/ui/help-tip";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ArrowDown, ArrowUp, Pencil, RotateCcw } from "lucide-react";
import { useUIStore } from "@/store";
import {
QUICK_ACTIONS_REGISTRY,
resolveQuickActions,
} from "./quick-actions-registry";
// Per-user honesty: the panel is a single-operator surface today. This
// customization is persisted per-browser via the existing localStorage-backed
// useUIStore — different browsers/profiles keep their own chosen set, which
// IS the "different people need different quick access buttons" mechanism
// for now. No server-side per-user storage is built.
/**
* Compact icon+label grid of the CEO's chosen quick actions, in their chosen
* order — replaces the old hardcoded QuickActionsBar. A gear/pencil affordance
* opens the customize dialog (below) to pick which actions show and reorder
* them.
*/
export function QuickActionsCard() {
const quickActionIds = useUIStore((s) => s.quickActionIds);
const actions = resolveQuickActions(quickActionIds);
return (
<div className="flex flex-wrap items-center gap-3">
{actions.length === 0 && (
<p className="py-4 text-center text-sm text-muted-foreground">
No quick actions selected use the pencil to add some.
</p>
)}
<div className="grid flex-1 grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-2">
{actions.map((action) => (
<HelpTip key={action.id} label={action.tip}>
<Link href={action.href} prefetch={false}>
<Button
variant="outline"
className="h-auto w-full flex-col gap-1.5 py-3"
>
<action.icon className="h-5 w-5" />
<span className="text-xs font-medium">{action.label}</span>
</Button>
</Link>
</HelpTip>
))}
</div>
<QuickActionsCustomizeDialog />
</div>
);
}
const CUSTOMIZE_LABEL = "Customize Quick Actions";
function QuickActionsCustomizeDialog() {
const [open, setOpen] = useState(false);
const quickActionIds = useUIStore((s) => s.quickActionIds);
const setQuickActionIds = useUIStore((s) => s.setQuickActionIds);
const resetQuickActionIds = useUIStore((s) => s.resetQuickActionIds);
// Stale ids (an action removed from the registry since it was picked) are
// dropped here too — the dialog only ever shows/reorders real actions.
const enabledIds = resolveQuickActions(quickActionIds).map((a) => a.id);
const enabledSet = new Set(enabledIds);
// Enabled actions first (in the user's chosen order), then everything else
// available to add, in registry order.
const orderedIds = [
...enabledIds,
...QUICK_ACTIONS_REGISTRY.filter((a) => !enabledSet.has(a.id)).map(
(a) => a.id,
),
];
const toggle = (id: string) => {
if (enabledSet.has(id)) {
setQuickActionIds(enabledIds.filter((x) => x !== id));
} else {
setQuickActionIds([...enabledIds, id]);
}
};
const move = (id: string, direction: -1 | 1) => {
const index = enabledIds.indexOf(id);
if (index === -1) return;
const target = index + direction;
if (target < 0 || target >= enabledIds.length) return;
const next = [...enabledIds];
[next[index], next[target]] = [next[target], next[index]];
setQuickActionIds(next);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<HelpTip label={CUSTOMIZE_LABEL}>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
aria-label={CUSTOMIZE_LABEL}
title={CUSTOMIZE_LABEL}
>
<Pencil className="h-4 w-4" />
</Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Customize Quick Actions</DialogTitle>
<DialogDescription>
Pick which shortcuts show on the Overview dashboard and reorder
them with the arrows. Saved to this browser only.
</DialogDescription>
</DialogHeader>
<div className="max-h-96 space-y-1 overflow-y-auto">
{orderedIds.map((id) => {
const action = QUICK_ACTIONS_REGISTRY.find((a) => a.id === id);
if (!action) return null;
const isEnabled = enabledSet.has(id);
const index = enabledIds.indexOf(id);
const isFirst = index === 0;
const isLast = index === enabledIds.length - 1;
return (
<div
key={id}
className="flex items-center gap-2 rounded-md px-1 py-1.5"
>
<Checkbox
checked={isEnabled}
onCheckedChange={() => toggle(id)}
aria-label={`Show ${action.label}`}
/>
<action.icon className="h-4 w-4 shrink-0 text-muted-foreground" />
<HelpTip label={action.tip}>
<span className="flex-1 truncate text-sm">
{action.label}
</span>
</HelpTip>
{isEnabled && (
<div className="flex shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
disabled={isFirst}
onClick={() => move(id, -1)}
aria-label={`Move ${action.label} earlier`}
>
<ArrowUp className="h-3.5 w-3.5" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>Move earlier</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
type="button"
variant="ghost"
size="icon"
className="h-6 w-6"
disabled={isLast}
onClick={() => move(id, 1)}
aria-label={`Move ${action.label} later`}
>
<ArrowDown className="h-3.5 w-3.5" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>Move later</TooltipContent>
</Tooltip>
</div>
)}
</div>
);
})}
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={resetQuickActionIds}
>
<RotateCcw className="h-4 w-4 mr-1.5" />
Reset to defaults
</Button>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,241 @@
import {
Sparkles,
ListTodo,
Kanban,
GitBranch,
Briefcase,
Share2,
Database,
Radio,
Bot,
BookOpen,
Shield,
Activity,
Building2,
Cpu,
Settings,
type LucideIcon,
} from "lucide-react";
export interface QuickAction {
id: string;
label: string;
icon: LucideIcon;
href: string;
tip: string;
}
// Static catalog of every quick-action destination the Overview dashboard can
// jump to. Derived from the panel's real route surface — the sidebar's
// canonical nav list (components/layout/sidebar.tsx) plus the tab-
// parameterized deep links each of those pages actually supports — never an
// invented route. The release/X/video/roadmap approval queues are
// deliberately absent: they already render directly on the Overview page
// itself (see command-center.tsx), so a "quick action" pointing at the page
// the user is already on would be dead weight.
export const QUICK_ACTIONS_REGISTRY: QuickAction[] = [
{
id: "new-task",
label: "New Task",
icon: Sparkles,
href: "/prompter",
tip: "Chat with Intake to draft and confirm a new task, including MegaTask batches",
},
{
id: "tasks",
label: "Tasks",
icon: ListTodo,
href: "/tasks",
tip: "Full task list — filter, search, and open any task's detail",
},
{
id: "kanban",
label: "Kanban",
icon: Kanban,
href: "/kanban",
tip: "Task board grouped by lifecycle status",
},
{
id: "git-repository",
label: "Repository",
icon: GitBranch,
href: "/git?tab=repository",
tip: "Branches, commits, and diffs across every project workspace",
},
{
id: "git-sessions",
label: "Work Sessions",
icon: GitBranch,
href: "/git?tab=sessions",
tip: "Active agent work sessions — branch, commits, and PR per task",
},
{
id: "workstation-products",
label: "Products",
icon: Briefcase,
href: "/workstation?tab=products",
tip: "Products the fleet ships against",
},
{
id: "workstation-projects",
label: "Projects",
icon: Briefcase,
href: "/workstation?tab=projects",
tip: "Manage repos, git tokens, and per-project settings",
},
{
id: "social",
label: "Social",
icon: Share2,
href: "/social",
tip: "X and TikTok post queues, plus the video pipeline",
},
{
id: "knowledge-base",
label: "Knowledge Base",
icon: Database,
href: "/knowledge-base",
tip: "Search the RAG corpus — playbooks, learnings, and vault notes",
},
{
id: "a2a",
label: "A2A",
icon: Radio,
href: "/a2a",
tip: "Live agent-to-agent message switchboard and history",
},
{
id: "agents",
label: "Agents",
icon: Bot,
href: "/agents",
tip: "Every agent's live state, spawn controls, and activity stream",
},
{
id: "journals",
label: "Journals",
icon: BookOpen,
href: "/journals",
tip: "Per-agent reflections and learnings",
},
{
id: "auditor",
label: "Auditor",
icon: Shield,
href: "/auditor",
tip: "Silent-observer quality flags and findings review queue",
},
{
id: "metrics-performance",
label: "Metrics",
icon: Activity,
href: "/metrics?tab=performance",
tip: "Task velocity, status counts, agent load, and team health",
},
{
id: "metrics-delivery",
label: "Delivery Metrics",
icon: Activity,
href: "/metrics?tab=delivery",
tip: "Cycle time, bottlenecks, and rework rate reconstructed from the audit log",
},
{
id: "metrics-token-usage",
label: "Token Usage",
icon: Activity,
href: "/metrics?tab=token-usage",
tip: "Token spend, cost projections, cache efficiency, and per-session detail",
},
{
id: "metrics-scorecards",
label: "Scorecards",
icon: Activity,
href: "/metrics?tab=scorecards",
tip: "Per-agent and per-team delivery scorecards",
},
{
id: "business-goals",
label: "Goals",
icon: Building2,
href: "/business?tab=goals",
tip: "CEO-owned charter — north star, brand voice, objectives, constraints",
},
{
id: "business-scorecard",
label: "Business Scorecard",
icon: Building2,
href: "/business?tab=scorecard",
tip: "Live delivery, spend, and speed metrics against the charter",
},
{
id: "business-secretary",
label: "Secretary",
icon: Bot,
href: "/business?tab=secretary",
tip: "Chat with your chief-of-staff and confirm or reject pending directives",
},
{
id: "business-pitches",
label: "Pitches",
icon: Building2,
href: "/business?tab=pitches",
tip: "Board-authored product pitches awaiting your decision",
},
{
id: "ai-providers",
label: "AI Providers",
icon: Cpu,
href: "/settings/ai-providers",
tip: "Model routing and per-role provider assignments",
},
{
id: "settings",
label: "Settings",
icon: Settings,
href: "/settings",
tip: "Feature flags, credentials, and panel preferences",
},
];
const REGISTRY_BY_ID: ReadonlyMap<string, QuickAction> = new Map(
QUICK_ACTIONS_REGISTRY.map((action) => [action.id, action]),
);
// Curated for the CEO's actual day-to-day workflow — replaces the old
// hardcoded QuickActionsBar (New Task dialog / Spawn Agent / Secretary /
// Journals / Auditor), which never surfaced Tasks, Kanban, Git, or Metrics
// at all despite those being the highest-traffic destinations.
// Includes every destination the legacy QuickActionsBar offered (secretary,
// journals, auditor) — absorbing it must not silently demote any of them out
// of a fresh install's default view.
export const DEFAULT_QUICK_ACTION_IDS: string[] = [
"new-task",
"tasks",
"kanban",
"git-repository",
"agents",
"a2a",
"business-secretary",
"journals",
"auditor",
"metrics-performance",
"settings",
];
/**
* Resolves a stored, ordered id list into real actions, dropping any id that
* no longer exists in the registry (a stale localStorage entry from a since-
* removed action) instead of crashing. Order-preserving.
*/
export function resolveQuickActions(ids: readonly string[]): QuickAction[] {
const resolved: QuickAction[] = [];
for (const id of ids) {
const action = REGISTRY_BY_ID.get(id);
if (action) resolved.push(action);
}
return resolved;
}
export function isKnownQuickActionId(id: string): boolean {
return REGISTRY_BY_ID.has(id);
}
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { useUIStore } from "../ui-store";
import { DEFAULT_QUICK_ACTION_IDS } from "@/components/dashboard/quick-actions-registry";
describe("ui-store persistence contract", () => {
it("persists quickActionIds through partialize (a key missing here silently stops persisting)", () => {
const partialize = useUIStore.persist.getOptions().partialize;
expect(partialize).toBeDefined();
const persisted = partialize!(useUIStore.getState()) as Record<string, unknown>;
expect(persisted.quickActionIds).toEqual(DEFAULT_QUICK_ACTION_IDS);
});
it("reset restores the exact default list after customization", () => {
useUIStore.getState().setQuickActionIds(["tasks"]);
expect(useUIStore.getState().quickActionIds).toEqual(["tasks"]);
useUIStore.getState().resetQuickActionIds();
expect(useUIStore.getState().quickActionIds).toEqual(DEFAULT_QUICK_ACTION_IDS);
});
});
+21
View File
@@ -1,6 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Team } from "@/types";
import { DEFAULT_QUICK_ACTION_IDS } from "@/components/dashboard/quick-actions-registry";
interface UIState {
// Sidebar
@@ -30,6 +31,12 @@ interface UIState {
autoRefresh: boolean;
refreshIntervalSeconds: number;
// Quick Actions (Overview dashboard) — ordered list of
// quick-actions-registry ids the CEO has chosen to show. Per-browser only,
// same persisted-preference idiom as everything else in this store; see
// quick-actions-card.tsx for the render/customize side.
quickActionIds: string[];
// Actions
toggleSidebar: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
@@ -42,6 +49,10 @@ interface UIState {
setSoundEnabled: (enabled: boolean) => void;
setAutoRefresh: (enabled: boolean) => void;
setRefreshIntervalSeconds: (seconds: number) => void;
// Quick Actions actions
setQuickActionIds: (ids: string[]) => void;
resetQuickActionIds: () => void;
}
export const useUIStore = create<UIState>()(
@@ -59,6 +70,9 @@ export const useUIStore = create<UIState>()(
autoRefresh: false, // default-off: never start a background poller unasked
refreshIntervalSeconds: 30,
// Quick Actions default
quickActionIds: DEFAULT_QUICK_ACTION_IDS,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
@@ -74,6 +88,11 @@ export const useUIStore = create<UIState>()(
setAutoRefresh: (enabled) => set({ autoRefresh: enabled }),
setRefreshIntervalSeconds: (seconds) =>
set({ refreshIntervalSeconds: seconds }),
// Quick Actions actions
setQuickActionIds: (ids) => set({ quickActionIds: ids }),
resetQuickActionIds: () =>
set({ quickActionIds: DEFAULT_QUICK_ACTION_IDS }),
}),
{
name: "roboco-ui-storage",
@@ -88,6 +107,8 @@ export const useUIStore = create<UIState>()(
soundEnabled: state.soundEnabled,
autoRefresh: state.autoRefresh,
refreshIntervalSeconds: state.refreshIntervalSeconds,
// Quick Actions
quickActionIds: state.quickActionIds,
}),
},
),