feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { LocalTask } from '../../api';
|
||||
import { matchText } from '../../lib/utils';
|
||||
import { COLUMN_LIST, SortMode, StatusColumn } from '../../lib/urlState';
|
||||
import { FilterBar } from './FilterBar';
|
||||
import { LocalTaskListItem } from './TaskListItem';
|
||||
import { RailPanel } from './RailPanel';
|
||||
|
||||
interface TaskListPanelProps {
|
||||
localTasks: LocalTask[];
|
||||
selectedStatus: 'all' | StatusColumn;
|
||||
sortMode: SortMode;
|
||||
searchQuery: string;
|
||||
activeTaskId: number | null;
|
||||
onStatusChange: (status: 'all' | StatusColumn) => void;
|
||||
onSortChange: (sort: SortMode) => void;
|
||||
onSearchChange: (q: string) => void;
|
||||
onSelectTask: (id: number) => void;
|
||||
onOpenCreate: () => void;
|
||||
/** 'rail' 時は RailPanel を render する。default 'list'。 */
|
||||
mode?: 'list' | 'rail';
|
||||
/** rail mode 時の「リストに戻る」ボタンで呼ばれる。 */
|
||||
onExitFocused?: () => void;
|
||||
}
|
||||
|
||||
export function TaskListPanel({
|
||||
localTasks,
|
||||
selectedStatus,
|
||||
sortMode,
|
||||
searchQuery,
|
||||
activeTaskId,
|
||||
onStatusChange,
|
||||
onSortChange,
|
||||
onSearchChange,
|
||||
onSelectTask,
|
||||
onOpenCreate,
|
||||
mode = 'list',
|
||||
onExitFocused,
|
||||
}: TaskListPanelProps) {
|
||||
if (mode === 'rail') {
|
||||
const localColumnsRail: Record<string, LocalTask[]> = COLUMN_LIST.reduce((acc, s) => {
|
||||
acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s);
|
||||
return acc;
|
||||
}, {} as Record<string, LocalTask[]>);
|
||||
const allRail = Object.values(localColumnsRail).flat();
|
||||
const baseRail = selectedStatus === 'all' ? allRail : localColumnsRail[selectedStatus] ?? [];
|
||||
const filteredRail = baseRail.filter(t =>
|
||||
matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery),
|
||||
);
|
||||
return (
|
||||
<RailPanel
|
||||
tasks={filteredRail}
|
||||
activeTaskId={activeTaskId}
|
||||
onSelectTask={onSelectTask}
|
||||
onOpenCreate={onOpenCreate}
|
||||
onExitFocused={onExitFocused ?? (() => {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const localColumns: Record<string, LocalTask[]> = COLUMN_LIST.reduce((acc, s) => {
|
||||
acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s);
|
||||
return acc;
|
||||
}, {} as Record<string, LocalTask[]>);
|
||||
|
||||
const allLocalTasks = Object.values(localColumns).flat();
|
||||
|
||||
const baseList: LocalTask[] =
|
||||
selectedStatus === 'all' ? allLocalTasks : localColumns[selectedStatus] ?? [];
|
||||
|
||||
const filtered = baseList.filter(t =>
|
||||
matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery)
|
||||
).sort((a, b) => {
|
||||
if (sortMode === 'title') {
|
||||
return a.title.localeCompare(b.title);
|
||||
}
|
||||
if (sortMode === 'status') {
|
||||
const aStatus = a.latestJob?.status ?? 'queued';
|
||||
const bStatus = b.latestJob?.status ?? 'queued';
|
||||
return aStatus.localeCompare(bStatus);
|
||||
}
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
|
||||
const counts: Record<string, number> = {};
|
||||
for (const s of COLUMN_LIST) {
|
||||
counts[s] = localColumns[s]?.length ?? 0;
|
||||
}
|
||||
const totalCount = allLocalTasks.length;
|
||||
const runningCount = counts.running ?? 0;
|
||||
const waitingCount = (counts.waiting_human ?? 0) + (counts.waiting_subtasks ?? 0);
|
||||
const failedCount = counts.failed ?? 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenCreate}
|
||||
className="w-full mb-3 px-3 py-2 bg-accent hover:bg-accent-deep active:scale-[0.98] active:bg-accent-deep text-accent-fg rounded-md text-xs font-semibold inline-flex items-center justify-center gap-1.5 transition-[transform,background-color,color] duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring"
|
||||
>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
新しい依頼
|
||||
</button>
|
||||
<div className="flex items-center gap-3 text-[10px] text-slate-500 px-0.5 pb-2.5 font-mono tabular-nums">
|
||||
<span><span className="font-semibold text-slate-700">{totalCount}</span> 件</span>
|
||||
<span aria-hidden="true" className="text-slate-300">·</span>
|
||||
<span><span className="font-semibold text-emerald-600">{runningCount}</span> 実行中</span>
|
||||
<span><span className="font-semibold text-amber-600">{waitingCount}</span> 待機中</span>
|
||||
{failedCount > 0 && (
|
||||
<span><span className="font-semibold text-red-600">{failedCount}</span> 失敗</span>
|
||||
)}
|
||||
</div>
|
||||
<FilterBar
|
||||
selectedStatus={selectedStatus}
|
||||
sortMode={sortMode}
|
||||
searchQuery={searchQuery}
|
||||
counts={counts}
|
||||
totalCount={totalCount}
|
||||
onStatusChange={onStatusChange}
|
||||
onSortChange={onSortChange}
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5 mt-2 overflow-y-auto flex-1 min-h-0 pr-0.5">
|
||||
{filtered.map(task => (
|
||||
<LocalTaskListItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
active={activeTaskId === task.id}
|
||||
onClick={() => onSelectTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<div className="text-[13px] text-slate-500 px-2 py-3">スレッドがありません</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user