Extract dashboard task action buttons
This commit is contained in:
@@ -6,9 +6,10 @@ import type {
|
|||||||
DashboardTask,
|
DashboardTask,
|
||||||
DashboardTaskAction,
|
DashboardTaskAction,
|
||||||
} from './api';
|
} from './api';
|
||||||
import { formatDate, taskActionsFor } from './dashboardHelpers';
|
import { formatDate } from './dashboardHelpers';
|
||||||
import { EmptyState } from './EmptyState';
|
import { EmptyState } from './EmptyState';
|
||||||
import type { Locale, Messages } from './i18n';
|
import type { Locale, Messages } from './i18n';
|
||||||
|
import { TaskActionButtons } from './TaskActionButtons';
|
||||||
|
|
||||||
export type InboxItem = DashboardOverview['inbox'][number];
|
export type InboxItem = DashboardOverview['inbox'][number];
|
||||||
export type InboxActionKey = `${string}:${DashboardInboxAction}`;
|
export type InboxActionKey = `${string}:${DashboardInboxAction}`;
|
||||||
@@ -103,7 +104,6 @@ function InboxCard({
|
|||||||
item.source === 'scheduled-task' && item.taskId
|
item.source === 'scheduled-task' && item.taskId
|
||||||
? tasks.find((task) => task.id === item.taskId)
|
? tasks.find((task) => task.id === item.taskId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const linkedTaskActions = linkedTask ? taskActionsFor(linkedTask) : [];
|
|
||||||
const inboxActions = inboxActionsFor(item);
|
const inboxActions = inboxActionsFor(item);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -148,25 +148,14 @@ function InboxCard({
|
|||||||
{item.taskId ? t.inbox.openTask : t.inbox.openRoom}
|
{item.taskId ? t.inbox.openTask : t.inbox.openRoom}
|
||||||
</a>
|
</a>
|
||||||
) : null}
|
) : null}
|
||||||
{linkedTask && linkedTaskActions.length > 0 ? (
|
{linkedTask ? (
|
||||||
<div className="task-actions inbox-actions">
|
<TaskActionButtons
|
||||||
{linkedTaskActions.map((action) => {
|
className="inbox-actions"
|
||||||
const actionKey = `${linkedTask.id}:${action}`;
|
onTaskAction={onTaskAction}
|
||||||
const busy = taskActionKey === actionKey;
|
task={linkedTask}
|
||||||
return (
|
taskActionKey={taskActionKey}
|
||||||
<button
|
t={t}
|
||||||
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>
|
|
||||||
) : null}
|
) : null}
|
||||||
{inboxActions.length > 0 ? (
|
{inboxActions.length > 0 ? (
|
||||||
<div className="task-actions inbox-actions">
|
<div className="task-actions inbox-actions">
|
||||||
|
|||||||
50
apps/dashboard/src/TaskActionButtons.test.ts
Normal file
50
apps/dashboard/src/TaskActionButtons.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
52
apps/dashboard/src/TaskActionButtons.tsx
Normal file
52
apps/dashboard/src/TaskActionButtons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,16 +11,14 @@ import type {
|
|||||||
import { EmptyState } from './EmptyState';
|
import { EmptyState } from './EmptyState';
|
||||||
import { localeTags, type Locale, type Messages } from './i18n';
|
import { localeTags, type Locale, type Messages } from './i18n';
|
||||||
import { redactSecretsForPreview } from './redaction';
|
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 TaskGroupKey = 'watchers' | 'scheduled' | 'paused' | 'completed';
|
||||||
type TaskResultTone = 'ok' | 'fail' | 'none';
|
type TaskResultTone = 'ok' | 'fail' | 'none';
|
||||||
|
|
||||||
export type TaskActionKey =
|
|
||||||
| 'create'
|
|
||||||
| `${string}:edit`
|
|
||||||
| `${string}:${DashboardTaskAction}`;
|
|
||||||
|
|
||||||
export interface RoomOption {
|
export interface RoomOption {
|
||||||
jid: string;
|
jid: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -69,13 +67,6 @@ interface TaskCardProps {
|
|||||||
t: Messages;
|
t: Messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TaskActionButtonsProps {
|
|
||||||
onTaskAction: (task: DashboardTask, action: DashboardTaskAction) => void;
|
|
||||||
task: DashboardTask;
|
|
||||||
taskActionKey: TaskActionKey | null;
|
|
||||||
t: Messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskDateProps {
|
interface TaskDateProps {
|
||||||
locale: Locale;
|
locale: Locale;
|
||||||
t: Messages;
|
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) {
|
function TaskTimeGrid({ locale, t, task }: TaskDateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="task-time-grid">
|
<div className="task-time-grid">
|
||||||
|
|||||||
Reference in New Issue
Block a user