// TaskList — FilterBar + LocalTaskListItem recreation const SORT_OPTIONS = [ { value: 'updated', label: '新しい順' }, { value: 'status', label: 'ステータス順' }, { value: 'title', label: 'タイトル順' }, ]; function SortMenu({ sort, onSort }) { const [open, setOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []); const current = SORT_OPTIONS.find(o => o.value === sort) || SORT_OPTIONS[0]; return (
{open && (
{SORT_OPTIONS.map(o => ( ))}
)}
); } function FilterBar({ status, onStatus, search, onSearch, sort, onSort, counts, total }) { const columns = ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled']; const chipStyle = (active) => ({ flexShrink: 0, padding: '6px 10px', borderRadius: 9999, fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', cursor: 'pointer', border: '1px solid ' + (active ? '#2563eb' : '#e2e8f0'), background: active ? '#eff6ff' : '#fff', color: active ? '#1d4ed8' : '#64748b', fontFamily: 'inherit', }); return (
onSearch(e.target.value)} placeholder="検索..." style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 13, fontFamily: 'inherit', color: '#0f172a', minWidth: 0, padding: '4px 0' }} />
{columns.map(s => ( ))}
); } function TaskItem({ task, active, onClick }) { return ( ); } function TaskList({ tasks, activeId, onSelect, filters, setFilters, onOpenCreate, loading, error, onRetry }) { const counts = {}; for (const s of ['queued', 'running', 'waiting_human', 'waiting_subtasks', 'retry', 'succeeded', 'failed', 'cancelled']) { counts[s] = tasks.filter(t => t.status === s).length; } const running = counts.running || 0; const waiting = (counts.waiting_human || 0) + (counts.waiting_subtasks || 0); const failed = counts.failed || 0; const filtered = tasks .filter(t => filters.status === 'all' || t.status === filters.status) .filter(t => !filters.search || (t.title + t.body).toLowerCase().includes(filters.search.toLowerCase())) .sort((a, b) => filters.sort === 'title' ? a.title.localeCompare(b.title) : b.updatedAt - a.updatedAt); const hasSearch = !!filters.search || filters.status !== 'all'; return (
{tasks.length} · {running} 実行中 {waiting} 待機 {failed > 0 && {failed} 失敗}
setFilters(f => ({ ...f, status: s }))} search={filters.search} onSearch={(q) => setFilters(f => ({ ...f, search: q }))} sort={filters.sort} onSort={(s) => setFilters(f => ({ ...f, sort: s }))} counts={counts} total={tasks.length} />
{loading && } {!loading && error && ( )} {!loading && !error && filtered.map(t => onSelect(t.id)} />)} {!loading && !error && filtered.length === 0 && ( hasSearch ? ( } title="該当するタスクはありません" hint="検索ワードやステータスフィルタを変えてみてください。" action={ } /> ) : ( } title="まだ依頼がありません" hint="左上の「新しい依頼」から最初のタスクを作成できます。" /> ) )}
); } window.TaskList = TaskList;