Merge pull request #82 from phj1081/codex/owner/ejclaw

Extract dashboard task action buttons
This commit is contained in:
Eyejoker
2026-04-28 21:15:13 +09:00
committed by GitHub
4 changed files with 116 additions and 65 deletions

View File

@@ -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}
</a>
) : null}
{linkedTask && linkedTaskActions.length > 0 ? (
<div className="task-actions inbox-actions">
{linkedTaskActions.map((action) => {
const actionKey = `${linkedTask.id}:${action}`;
const busy = taskActionKey === actionKey;
return (
<button
aria-busy={busy || undefined}
className={`task-action task-action-${action}${busy ? ' is-busy' : ''}`}
disabled={busy}
key={action}
onClick={() => onTaskAction(linkedTask, action)}
type="button"
>
{busy ? t.tasks.actions.busy : t.tasks.actions[action]}
</button>
);
})}
</div>
{linkedTask ? (
<TaskActionButtons
className="inbox-actions"
onTaskAction={onTaskAction}
task={linkedTask}
taskActionKey={taskActionKey}
t={t}
/>
) : null}
{inboxActions.length > 0 ? (
<div className="task-actions inbox-actions">

View File

@@ -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);
});
});

View File

@@ -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 (
<div className={rootClassName}>
{taskActions.map((action) => {
const actionKey: TaskActionKey = `${task.id}:${action}`;
const busy = taskActionKey === actionKey;
return (
<button
aria-busy={busy || undefined}
className={`task-action task-action-${action}${busy ? ' is-busy' : ''}`}
disabled={busy}
key={action}
onClick={() => onTaskAction(task, action)}
type="button"
>
{busy ? t.tasks.actions.busy : t.tasks.actions[action]}
</button>
);
})}
</div>
);
}

View File

@@ -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 (
<div className="task-actions">
{taskActions.map((action) => {
const actionKey: TaskActionKey = `${task.id}:${action}`;
const busy = taskActionKey === actionKey;
return (
<button
aria-busy={busy || undefined}
className={`task-action task-action-${action}${busy ? ' is-busy' : ''}`}
disabled={busy}
key={action}
onClick={() => onTaskAction(task, action)}
type="button"
>
{busy ? t.tasks.actions.busy : t.tasks.actions[action]}
</button>
);
})}
</div>
);
}
function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
return (
<div className="task-time-grid">