From a8ae4aece495ac818500a4f891695ca70cd66237 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Tue, 28 Apr 2026 21:09:10 +0900 Subject: [PATCH] Extract dashboard task action buttons --- apps/dashboard/src/InboxPanel.tsx | 31 ++++-------- apps/dashboard/src/TaskActionButtons.test.ts | 50 +++++++++++++++++++ apps/dashboard/src/TaskActionButtons.tsx | 52 ++++++++++++++++++++ apps/dashboard/src/TaskPanel.tsx | 48 ++---------------- 4 files changed, 116 insertions(+), 65 deletions(-) create mode 100644 apps/dashboard/src/TaskActionButtons.test.ts create mode 100644 apps/dashboard/src/TaskActionButtons.tsx diff --git a/apps/dashboard/src/InboxPanel.tsx b/apps/dashboard/src/InboxPanel.tsx index ed4b76d..ca66edf 100644 --- a/apps/dashboard/src/InboxPanel.tsx +++ b/apps/dashboard/src/InboxPanel.tsx @@ -6,9 +6,10 @@ import type { DashboardTask, DashboardTaskAction, } from './api'; -import { formatDate, taskActionsFor } from './dashboardHelpers'; +import { formatDate } from './dashboardHelpers'; import { EmptyState } from './EmptyState'; import type { Locale, Messages } from './i18n'; +import { TaskActionButtons } from './TaskActionButtons'; export type InboxItem = DashboardOverview['inbox'][number]; export type InboxActionKey = `${string}:${DashboardInboxAction}`; @@ -103,7 +104,6 @@ function InboxCard({ item.source === 'scheduled-task' && item.taskId ? tasks.find((task) => task.id === item.taskId) : undefined; - const linkedTaskActions = linkedTask ? taskActionsFor(linkedTask) : []; const inboxActions = inboxActionsFor(item); return ( @@ -148,25 +148,14 @@ function InboxCard({ {item.taskId ? t.inbox.openTask : t.inbox.openRoom} ) : null} - {linkedTask && linkedTaskActions.length > 0 ? ( -
- {linkedTaskActions.map((action) => { - const actionKey = `${linkedTask.id}:${action}`; - const busy = taskActionKey === actionKey; - return ( - - ); - })} -
+ {linkedTask ? ( + ) : null} {inboxActions.length > 0 ? (
diff --git a/apps/dashboard/src/TaskActionButtons.test.ts b/apps/dashboard/src/TaskActionButtons.test.ts new file mode 100644 index 0000000..5a000da --- /dev/null +++ b/apps/dashboard/src/TaskActionButtons.test.ts @@ -0,0 +1,50 @@ +import { createElement } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import type { DashboardTask } from './api'; +import { messages } from './i18n'; +import { TaskActionButtons } from './TaskActionButtons'; + +const t = messages.en; + +const task: DashboardTask = { + agentType: 'codex', + chatJid: 'room-1', + ciMetadata: null, + ciProvider: null, + contextMode: 'group', + createdAt: '2026-04-28T04:00:00.000Z', + groupFolder: 'eyejokerdb', + id: 'task-1', + isWatcher: false, + lastResult: null, + lastRun: null, + nextRun: '2026-04-28T05:00:00.000Z', + promptLength: 42, + promptPreview: 'Run production check', + scheduleType: 'interval', + scheduleValue: '10m', + status: 'active', + suspendedUntil: null, +}; + +describe('TaskActionButtons', () => { + it('renders task actions with shared busy state styling', () => { + const html = renderToStaticMarkup( + createElement(TaskActionButtons, { + className: 'inbox-actions', + onTaskAction: () => {}, + task, + taskActionKey: 'task-1:pause', + t, + }), + ); + + expect(html).toContain('task-actions inbox-actions'); + expect(html).toContain('task-action task-action-pause is-busy'); + expect(html).toContain('aria-busy="true"'); + expect(html).toContain(t.tasks.actions.busy); + expect(html).toContain(t.tasks.actions.cancel); + }); +}); diff --git a/apps/dashboard/src/TaskActionButtons.tsx b/apps/dashboard/src/TaskActionButtons.tsx new file mode 100644 index 0000000..95b5d67 --- /dev/null +++ b/apps/dashboard/src/TaskActionButtons.tsx @@ -0,0 +1,52 @@ +import type { DashboardTask, DashboardTaskAction } from './api'; +import { taskActionsFor } from './dashboardHelpers'; +import type { Messages } from './i18n'; + +export type TaskActionKey = + | 'create' + | `${string}:edit` + | `${string}:${DashboardTaskAction}`; + +interface TaskActionButtonsProps { + className?: string; + onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; + task: DashboardTask; + taskActionKey: string | null; + t: Messages; +} + +export function TaskActionButtons({ + className, + onTaskAction, + task, + taskActionKey, + t, +}: TaskActionButtonsProps) { + const taskActions = taskActionsFor(task); + if (taskActions.length === 0) return null; + + const rootClassName = className + ? `task-actions ${className}` + : 'task-actions'; + + return ( +
+ {taskActions.map((action) => { + const actionKey: TaskActionKey = `${task.id}:${action}`; + const busy = taskActionKey === actionKey; + return ( + + ); + })} +
+ ); +} diff --git a/apps/dashboard/src/TaskPanel.tsx b/apps/dashboard/src/TaskPanel.tsx index 4cd32f3..c197c48 100644 --- a/apps/dashboard/src/TaskPanel.tsx +++ b/apps/dashboard/src/TaskPanel.tsx @@ -11,16 +11,14 @@ import type { import { EmptyState } from './EmptyState'; import { localeTags, type Locale, type Messages } from './i18n'; import { redactSecretsForPreview } from './redaction'; -import { statusLabel, taskActionsFor } from './dashboardHelpers'; +import { statusLabel } from './dashboardHelpers'; +import { TaskActionButtons, type TaskActionKey } from './TaskActionButtons'; + +export type { TaskActionKey } from './TaskActionButtons'; type TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed'; type TaskResultTone = 'ok' | 'fail' | 'none'; -export type TaskActionKey = - | 'create' - | `${string}:edit` - | `${string}:${DashboardTaskAction}`; - export interface RoomOption { jid: string; name: string; @@ -69,13 +67,6 @@ interface TaskCardProps { t: Messages; } -interface TaskActionButtonsProps { - onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void; - task: DashboardTask; - taskActionKey: TaskActionKey | null; - t: Messages; -} - interface TaskDateProps { locale: Locale; t: Messages; @@ -341,37 +332,6 @@ function TaskCreateForm({ ); } -function TaskActionButtons({ - onTaskAction, - task, - taskActionKey, - t, -}: TaskActionButtonsProps) { - const taskActions = taskActionsFor(task); - if (taskActions.length === 0) return null; - - return ( -
- {taskActions.map((action) => { - const actionKey: TaskActionKey = `${task.id}:${action}`; - const busy = taskActionKey === actionKey; - return ( - - ); - })} -
- ); -} - function TaskTimeGrid({ locale, t, task }: TaskDateProps) { return (