mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -8652,7 +8652,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/app/(dashboard)/{agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
| `panel/src/app/(dashboard)/{agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx` | Per-domain pages |
|
||||||
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
||||||
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present |
|
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present |
|
||||||
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
||||||
| `panel/src/components/metrics/` | delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table |
|
| `panel/src/components/metrics/` | delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table |
|
||||||
| `panel/src/components/kanban/{core,shared,views}/` | core: kanban-board/column/card + bypass-preconditions; views: dev/qa/pm/pr-review kanban |
|
| `panel/src/components/kanban/{core,shared,views}/` | core: kanban-board/column/card + bypass-preconditions; views: dev/qa/pm/pr-review kanban |
|
||||||
| `panel/src/components/prompter/` | intake-form, chat-messages, chat-composer, draft-proposal-card, batch-review-card, success-card, board-review-sent-card |
|
| `panel/src/components/prompter/` | intake-form, chat-messages, chat-composer, draft-proposal-card, batch-review-card, success-card, board-review-sent-card |
|
||||||
@@ -8729,6 +8729,8 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
||||||
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
||||||
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
||||||
|
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids |
|
||||||
|
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
||||||
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
||||||
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
||||||
| `useCleanupBranches` / `handleCleanupBranches` | hook | `hooks/use-git.ts` / `hooks/use-git-browser.ts` | Mutation over `POST /git/branches/cleanup`; the browser hook tracks a per-project cursor ref so a repeat click resumes a truncated sweep instead of re-scanning the first window |
|
| `useCleanupBranches` / `handleCleanupBranches` | hook | `hooks/use-git.ts` / `hooks/use-git-browser.ts` | Mutation over `POST /git/branches/cleanup`; the browser hook tracks a per-project cursor ref so a repeat click resumes a truncated sweep instead of re-scanning the first window |
|
||||||
@@ -8792,7 +8794,7 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
||||||
│ └── {agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
│ └── {agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
||||||
├── src/components/
|
├── src/components/
|
||||||
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions, recent-activity)
|
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
||||||
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
||||||
│ ├── kanban/
|
│ ├── kanban/
|
||||||
│ │ ├── core/ (kanban-board/column/card + bypass-preconditions)
|
│ │ ├── core/ (kanban-board/column/card + bypass-preconditions)
|
||||||
@@ -8912,6 +8914,7 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
|||||||
> - (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Wave 2 hygiene + charts: `/work-sessions` route now redirects to `/git?tab=sessions` (moved under Git as a "Work Sessions" tab, `git-page.tsx` gains a `Tabs`); `GitActionsPanel` gains a confirm-gated "Clean Up Stale Branches" button (`useCleanupBranches`, cursor-resumable); `WorkSessionsView`'s filters moved from URL params to local state (ScrollRestoration bounce fix); new `SessionTrendChart` / `CostTrendChart` / `SpendTrendChart`.
|
> - (open PR #548, branch `feature/wave-2-hygiene-charts`, 2026-07-17) Wave 2 hygiene + charts: `/work-sessions` route now redirects to `/git?tab=sessions` (moved under Git as a "Work Sessions" tab, `git-page.tsx` gains a `Tabs`); `GitActionsPanel` gains a confirm-gated "Clean Up Stale Branches" button (`useCleanupBranches`, cursor-resumable); `WorkSessionsView`'s filters moved from URL params to local state (ScrollRestoration bounce fix); new `SessionTrendChart` / `CostTrendChart` / `SpendTrendChart`.
|
||||||
> - `dd4cb7f1` Wave 4: Workstation page (PR #549, 2026-07-17) — Products + Projects merged into one `/workstation?tab=` page (Products first); content extracted byte-faithfully into `components/products/products-view.tsx` + `components/projects/projects-view.tsx`; `products/page.tsx` + `projects/page.tsx` become redirect shims; the two sidebar entries collapse into one "Workstation" entry (`Briefcase` icon); `task-metadata.tsx`'s project-card link retargets to `/workstation?tab=projects`; `ProjectsView`'s q/cell/inactive filters move from URL params to local `useState` (scroll-bounce prevention, trades away shareable filtered-view links).
|
> - `dd4cb7f1` Wave 4: Workstation page (PR #549, 2026-07-17) — Products + Projects merged into one `/workstation?tab=` page (Products first); content extracted byte-faithfully into `components/products/products-view.tsx` + `components/projects/projects-view.tsx`; `products/page.tsx` + `projects/page.tsx` become redirect shims; the two sidebar entries collapse into one "Workstation" entry (`Briefcase` icon); `task-metadata.tsx`'s project-card link retargets to `/workstation?tab=projects`; `ProjectsView`'s q/cell/inactive filters move from URL params to local `useState` (scroll-bounce prevention, trades away shareable filtered-view links).
|
||||||
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
||||||
|
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
||||||
|
|
||||||
## Regression Risks
|
## Regression Risks
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -22,7 +22,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
| `panel/src/app/(auth)/login/page.tsx` | Cloud-auth login form (email/password → `useLogin` → `/auth/login`); only reachable/relevant once `proxy.ts` starts gating the `(dashboard)` group |
|
||||||
| `panel/src/app/(tg)/layout.tsx` + `(tg)/tg/page.tsx` | Telegram Mini App cockpit at `/tg`: slim shell (no sidebar/header, `next/script` loads the Telegram WebApp bridge `afterInteractive`) + bootstrap page that resolves `window.Telegram.WebApp`, POSTs its `initData` to `/telegram/webapp-auth` unconditionally, then renders the tabbed cockpit (or an "Open from Telegram" / error state) |
|
| `panel/src/app/(tg)/layout.tsx` + `(tg)/tg/page.tsx` | Telegram Mini App cockpit at `/tg`: slim shell (no sidebar/header, `next/script` loads the Telegram WebApp bridge `afterInteractive`) + bootstrap page that resolves `window.Telegram.WebApp`, POSTs its `initData` to `/telegram/webapp-auth` unconditionally, then renders the tabbed cockpit (or an "Open from Telegram" / error state) |
|
||||||
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present; matcher excludes `tg(?:/|$)` — the Mini App authenticates via Telegram `initData`, not the password cookie, so it must never be redirected to `/login` |
|
| `panel/src/proxy.ts` | Next 16's rename of `middleware.ts`: probes `/auth/status` (docker-internal orchestrator URL, fails open to "off" on any error/timeout) and redirects to `/login` when cloud auth is on and no session cookie is present; matcher excludes `tg(?:/|$)` — the Mini App authenticates via Telegram `initData`, not the password cookie, so it must never be redirected to `/login` |
|
||||||
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
| `panel/src/components/dashboard/` | Overview cards: command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity, `x-post-queue.tsx`, `roadmap-review-queue.tsx` |
|
||||||
| `panel/src/components/metrics/` | delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table |
|
| `panel/src/components/metrics/` | delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table |
|
||||||
| `panel/src/components/kanban/{core,shared,views}/` | core: kanban-board/column/card + bypass-preconditions; views: dev/qa/pm/pr-review kanban |
|
| `panel/src/components/kanban/{core,shared,views}/` | core: kanban-board/column/card + bypass-preconditions; views: dev/qa/pm/pr-review kanban |
|
||||||
| `panel/src/components/prompter/` | intake-form, chat-messages, chat-composer, draft-proposal-card, batch-review-card, success-card, board-review-sent-card |
|
| `panel/src/components/prompter/` | intake-form, chat-messages, chat-composer, draft-proposal-card, batch-review-card, success-card, board-review-sent-card |
|
||||||
@@ -102,6 +102,8 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.14.0) is the s
|
|||||||
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
| `XCredentialsCard` | comp | `components/settings/x-credentials-card.tsx` | Write-only entry of the 4 OAuth 1.0a secrets; set-all-4 or clear-all-4 |
|
||||||
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
| `RequiredNotesDialog` | comp | `components/ui/required-notes-dialog.tsx` | Reusable notes-gated confirm; submit disabled on empty/whitespace |
|
||||||
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
| `CommandCenter` | comp | `components/dashboard/command-center.tsx` | Overview page body; composes all dashboard cards |
|
||||||
|
| `QUICK_ACTIONS_REGISTRY` | const | `components/dashboard/quick-actions-registry.ts` | 23-action catalog (id/label/icon/href/tip) derived from the sidebar route surface + tab-parameterized deep links; `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy `QuickActionsBar` destination; `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids |
|
||||||
|
| `QuickActionsCard` / `QuickActionsCustomizeDialog` | comp | `components/dashboard/quick-actions-card.tsx` | Chosen-order icon+label grid off `useUIStore.quickActionIds` (empty-state message when none selected); pencil-icon dialog toggles/reorders via checkbox + up/down arrows and resets to defaults, persisted through `setQuickActionIds`/`resetQuickActionIds` |
|
||||||
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
| `DeliveryTabContent` | comp | `components/metrics/delivery-tab.tsx` | Cycle-time/bottleneck/rework/scorecard panels |
|
||||||
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
| `GitActionsPanel` | comp | `components/git/git-actions-panel.tsx` | Commit/push/PR/rebase actions + destructive-confirm "Clean Up Stale Branches" (`AlertDialog`) |
|
||||||
| `useCleanupBranches` / `handleCleanupBranches` | hook | `hooks/use-git.ts` / `hooks/use-git-browser.ts` | Mutation over `POST /git/branches/cleanup`; the browser hook tracks a per-project cursor ref so a repeat click resumes a truncated sweep instead of re-scanning the first window |
|
| `useCleanupBranches` / `handleCleanupBranches` | hook | `hooks/use-git.ts` / `hooks/use-git-browser.ts` | Mutation over `POST /git/branches/cleanup`; the browser hook tracks a per-project cursor ref so a repeat click resumes a truncated sweep instead of re-scanning the first window |
|
||||||
@@ -168,7 +170,7 @@ panel/ (Next.js 16, package roboco-panel v0.14.0)
|
|||||||
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
│ ├── workstation/page.tsx (Products + Projects tabs; projects/, products/ page.tsx now redirect here)
|
||||||
│ └── {agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
│ └── {agents,business,journals,git,knowledge-base,auditor,work-sessions,notifications}/page.tsx
|
||||||
├── src/components/
|
├── src/components/
|
||||||
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions, recent-activity)
|
│ ├── dashboard/ (command-center, key-metrics, release-proposal, playbook-review-queue, ceo-approval-queue, pr-review-queue, x-post-queue, roadmap-review-queue, usage-overview, team-health, active-blockers, auditor-alerts, strategy-signals, quick-actions-card, quick-actions-registry, recent-activity)
|
||||||
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
│ ├── metrics/ (delivery-tab, usage-time-series-chart, agent/team-usage-chart, model-usage-donut, sessions-table)
|
||||||
│ ├── kanban/
|
│ ├── kanban/
|
||||||
│ │ ├── core/ (kanban-board/column/card + bypass-preconditions)
|
│ │ ├── core/ (kanban-board/column/card + bypass-preconditions)
|
||||||
@@ -293,6 +295,7 @@ Deliberately **not** on this card (compose/env-coupled, unsafe for a runtime tog
|
|||||||
> - `dd4cb7f1` Wave 4: Workstation page (PR #549, 2026-07-17) — Products + Projects merged into one `/workstation?tab=` page (Products first); content extracted byte-faithfully into `components/products/products-view.tsx` + `components/projects/projects-view.tsx`; `products/page.tsx` + `projects/page.tsx` become redirect shims; the two sidebar entries collapse into one "Workstation" entry (`Briefcase` icon); `task-metadata.tsx`'s project-card link retargets to `/workstation?tab=projects`; `ProjectsView`'s q/cell/inactive filters move from URL params to local `useState` (scroll-bounce prevention, trades away shareable filtered-view links).
|
> - `dd4cb7f1` Wave 4: Workstation page (PR #549, 2026-07-17) — Products + Projects merged into one `/workstation?tab=` page (Products first); content extracted byte-faithfully into `components/products/products-view.tsx` + `components/projects/projects-view.tsx`; `products/page.tsx` + `projects/page.tsx` become redirect shims; the two sidebar entries collapse into one "Workstation" entry (`Briefcase` icon); `task-metadata.tsx`'s project-card link retargets to `/workstation?tab=projects`; `ProjectsView`'s q/cell/inactive filters move from URL params to local `useState` (scroll-bounce prevention, trades away shareable filtered-view links).
|
||||||
> - `e16fb634`+`8d727785` (2026-07-18, PR #554, Telegram V3 Mini App) — new `(tg)` route group (`layout.tsx` + `tg/page.tsx`) and `components/tg/{tg-tab-bar,tg-approvals-tab,tg-inbox-tab,tg-board-tab,tg-chat-tab}.tsx`; new `components/tasks/mobile-task-board.tsx` (read-only, grouped-by-status) and `lib/telegram/webapp.ts` (WebApp bridge + `waitForTelegramWebApp` poll); `a2a-new-dm-dialog.tsx` exports `EXCLUDE_NON_DM_ROLES` for `tg-chat-tab.tsx` to reuse; `use-a2a-live.ts`'s `useA2AMessages` gains an optional `refetchInterval` (the cockpit polls instead of using WS); `proxy.ts` matcher excludes `tg(?:/|$)` (anchored in the fix commit — see Gotchas). Backend companion: `POST /api/telegram/webapp-auth` — see `docs/map/api-routes-schemas.md`.
|
> - `e16fb634`+`8d727785` (2026-07-18, PR #554, Telegram V3 Mini App) — new `(tg)` route group (`layout.tsx` + `tg/page.tsx`) and `components/tg/{tg-tab-bar,tg-approvals-tab,tg-inbox-tab,tg-board-tab,tg-chat-tab}.tsx`; new `components/tasks/mobile-task-board.tsx` (read-only, grouped-by-status) and `lib/telegram/webapp.ts` (WebApp bridge + `waitForTelegramWebApp` poll); `a2a-new-dm-dialog.tsx` exports `EXCLUDE_NON_DM_ROLES` for `tg-chat-tab.tsx` to reuse; `use-a2a-live.ts`'s `useA2AMessages` gains an optional `refetchInterval` (the cockpit polls instead of using WS); `proxy.ts` matcher excludes `tg(?:/|$)` (anchored in the fix commit — see Gotchas). Backend companion: `POST /api/telegram/webapp-auth` — see `docs/map/api-routes-schemas.md`.
|
||||||
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
> - `49a87c2b`+`9a33efba` (2026-07-18, PR #556, Workstation card grids) — new `product-card-grid.tsx`/`project-card-grid.tsx` (same intrinsic `grid-cols-[repeat(auto-fill,minmax(17rem,1fr))]` sizing as `agent-grid.tsx`), reusing `CellsList`/`ProgressCell` (newly exported from `product-table.tsx`) and `getExternalUrl`/badge renderers (newly exported from `project-table.tsx`); each Workstation surface gains a Cards\|Table toggle (default Cards, persisted per-surface via `ui-store`'s new `productsView`/`projectsView`) plus a name/cell(-count) sort control that only applies in card view — the table keeps its own unsorted render. `sortProducts`/`sortProjects` (both exported for direct unit tests) use a direction multiplier rather than sort-then-reverse, since reversing also flips the relative order of ties, and `sortProjects`' cell-key comparator falls back to the raw `assigned_cell` value for a backend `Team` outside the panel's `teamLabels` map instead of crashing; the fix commit tightened both to this shape after adversarial review caught the initial sort-then-reverse cut.
|
||||||
|
> - `97306fe7`+`151d6e0f` (2026-07-18, PR #557, customizable Quick Actions) — the hardcoded `QuickActionsBar` (deleted) is replaced by `QuickActionsCard` (`components/dashboard/quick-actions-card.tsx`), backed by a new 23-action `QUICK_ACTIONS_REGISTRY` (`components/dashboard/quick-actions-registry.ts`, id/label/icon/href/tip per entry, tab-parameterized deep links e.g. `/git?tab=sessions`, `/metrics?tab=delivery`); `DEFAULT_QUICK_ACTION_IDS` absorbs every legacy bar destination (secretary/journals/auditor) so the swap drops nothing from a fresh install's default view. Selection persists as `quickActionIds` on `useUIStore` (new key in `partialize`, contract-tested); `resolveQuickActions`/`isKnownQuickActionId` drop stale persisted ids instead of crashing. A pencil-icon `QuickActionsCustomizeDialog` (inline in `quick-actions-card.tsx`) lets the CEO show/hide + reorder (up/down arrows) + reset to defaults; the card shows an empty-state message when every action is hidden. `command-center.tsx` and its test swap `QuickActionsBar`/`QuickActionsBarStub` references for `QuickActionsCard`/`QuickActionsCardStub`.
|
||||||
|
|
||||||
## Regression Risks
|
## Regression Risks
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ vi.mock("../active-blockers-panel", () => ({
|
|||||||
vi.mock("../recent-activity-feed", () => ({
|
vi.mock("../recent-activity-feed", () => ({
|
||||||
RecentActivityFeed: () => <div>RecentActivityFeedStub</div>,
|
RecentActivityFeed: () => <div>RecentActivityFeedStub</div>,
|
||||||
}));
|
}));
|
||||||
vi.mock("../quick-actions-bar", () => ({
|
vi.mock("../quick-actions-card", () => ({
|
||||||
QuickActionsBar: () => <div>QuickActionsBarStub</div>,
|
QuickActionsCard: () => <div>QuickActionsCardStub</div>,
|
||||||
}));
|
}));
|
||||||
vi.mock("../ceo-approval-queue", () => ({
|
vi.mock("../ceo-approval-queue", () => ({
|
||||||
CeoApprovalQueue: () => <div>CeoApprovalQueueStub</div>,
|
CeoApprovalQueue: () => <div>CeoApprovalQueueStub</div>,
|
||||||
@@ -111,7 +111,7 @@ describe("CommandCenter", () => {
|
|||||||
it("renders every section", () => {
|
it("renders every section", () => {
|
||||||
render(<CommandCenter />);
|
render(<CommandCenter />);
|
||||||
for (const stub of [
|
for (const stub of [
|
||||||
"QuickActionsBarStub",
|
"QuickActionsCardStub",
|
||||||
"KeyMetricsPanelStub",
|
"KeyMetricsPanelStub",
|
||||||
"AuditorAlertsPanelStub",
|
"AuditorAlertsPanelStub",
|
||||||
"UsageOverviewPanelStub",
|
"UsageOverviewPanelStub",
|
||||||
@@ -135,7 +135,7 @@ describe("CommandCenter", () => {
|
|||||||
it("orders sections top to bottom: quick actions/key cards, team health, then the rest", () => {
|
it("orders sections top to bottom: quick actions/key cards, team health, then the rest", () => {
|
||||||
render(<CommandCenter />);
|
render(<CommandCenter />);
|
||||||
const order = [
|
const order = [
|
||||||
"QuickActionsBarStub",
|
"QuickActionsCardStub",
|
||||||
"KeyMetricsPanelStub",
|
"KeyMetricsPanelStub",
|
||||||
"TeamHealthCardsStub",
|
"TeamHealthCardsStub",
|
||||||
"CeoApprovalQueueStub",
|
"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 { AuditorAlertsPanel } from "./auditor-alerts-panel";
|
||||||
import { ActiveBlockersPanel } from "./active-blockers-panel";
|
import { ActiveBlockersPanel } from "./active-blockers-panel";
|
||||||
import { RecentActivityFeed } from "./recent-activity-feed";
|
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 { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||||
import { PrReviewQueue } from "./pr-review-queue";
|
import { PrReviewQueue } from "./pr-review-queue";
|
||||||
import { ReleaseProposalCard } from "./release-proposal-card";
|
import { ReleaseProposalCard } from "./release-proposal-card";
|
||||||
@@ -154,7 +154,7 @@ export function CommandCenter() {
|
|||||||
Quick Actions
|
Quick Actions
|
||||||
</h2>
|
</h2>
|
||||||
</HelpTip>
|
</HelpTip>
|
||||||
<QuickActionsBar />
|
<QuickActionsCard />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-4">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export { AuditorAlertsPanel } from "./auditor-alerts-panel";
|
|||||||
export { ActiveBlockersPanel } from "./active-blockers-panel";
|
export { ActiveBlockersPanel } from "./active-blockers-panel";
|
||||||
export { RecentActivityFeed } from "./recent-activity-feed";
|
export { RecentActivityFeed } from "./recent-activity-feed";
|
||||||
export { ActivityItem } from "./activity-item";
|
export { ActivityItem } from "./activity-item";
|
||||||
export { QuickActionsBar } from "./quick-actions-bar";
|
export { QuickActionsCard } from "./quick-actions-card";
|
||||||
export { HealthIndicator } from "./health-indicator";
|
export { HealthIndicator } from "./health-indicator";
|
||||||
export { CeoApprovalQueue } from "./ceo-approval-queue";
|
export { CeoApprovalQueue } from "./ceo-approval-queue";
|
||||||
export { ReleaseProposalCard } from "./release-proposal-card";
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import type { Team } from "@/types";
|
import type { Team } from "@/types";
|
||||||
|
import { DEFAULT_QUICK_ACTION_IDS } from "@/components/dashboard/quick-actions-registry";
|
||||||
|
|
||||||
interface UIState {
|
interface UIState {
|
||||||
// Sidebar
|
// Sidebar
|
||||||
@@ -30,6 +31,12 @@ interface UIState {
|
|||||||
autoRefresh: boolean;
|
autoRefresh: boolean;
|
||||||
refreshIntervalSeconds: number;
|
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
|
// Actions
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||||
@@ -42,6 +49,10 @@ interface UIState {
|
|||||||
setSoundEnabled: (enabled: boolean) => void;
|
setSoundEnabled: (enabled: boolean) => void;
|
||||||
setAutoRefresh: (enabled: boolean) => void;
|
setAutoRefresh: (enabled: boolean) => void;
|
||||||
setRefreshIntervalSeconds: (seconds: number) => void;
|
setRefreshIntervalSeconds: (seconds: number) => void;
|
||||||
|
|
||||||
|
// Quick Actions actions
|
||||||
|
setQuickActionIds: (ids: string[]) => void;
|
||||||
|
resetQuickActionIds: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUIStore = create<UIState>()(
|
export const useUIStore = create<UIState>()(
|
||||||
@@ -59,6 +70,9 @@ export const useUIStore = create<UIState>()(
|
|||||||
autoRefresh: false, // default-off: never start a background poller unasked
|
autoRefresh: false, // default-off: never start a background poller unasked
|
||||||
refreshIntervalSeconds: 30,
|
refreshIntervalSeconds: 30,
|
||||||
|
|
||||||
|
// Quick Actions default
|
||||||
|
quickActionIds: DEFAULT_QUICK_ACTION_IDS,
|
||||||
|
|
||||||
toggleSidebar: () =>
|
toggleSidebar: () =>
|
||||||
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||||
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
|
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
|
||||||
@@ -74,6 +88,11 @@ export const useUIStore = create<UIState>()(
|
|||||||
setAutoRefresh: (enabled) => set({ autoRefresh: enabled }),
|
setAutoRefresh: (enabled) => set({ autoRefresh: enabled }),
|
||||||
setRefreshIntervalSeconds: (seconds) =>
|
setRefreshIntervalSeconds: (seconds) =>
|
||||||
set({ refreshIntervalSeconds: seconds }),
|
set({ refreshIntervalSeconds: seconds }),
|
||||||
|
|
||||||
|
// Quick Actions actions
|
||||||
|
setQuickActionIds: (ids) => set({ quickActionIds: ids }),
|
||||||
|
resetQuickActionIds: () =>
|
||||||
|
set({ quickActionIds: DEFAULT_QUICK_ACTION_IDS }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "roboco-ui-storage",
|
name: "roboco-ui-storage",
|
||||||
@@ -88,6 +107,8 @@ export const useUIStore = create<UIState>()(
|
|||||||
soundEnabled: state.soundEnabled,
|
soundEnabled: state.soundEnabled,
|
||||||
autoRefresh: state.autoRefresh,
|
autoRefresh: state.autoRefresh,
|
||||||
refreshIntervalSeconds: state.refreshIntervalSeconds,
|
refreshIntervalSeconds: state.refreshIntervalSeconds,
|
||||||
|
// Quick Actions
|
||||||
|
quickActionIds: state.quickActionIds,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user