feat(console): 任务树筛选/折叠/统一标题 + 侧栏全局Agent卡片/拖拽排序/全局设置弹框/记忆项目 + 状态点真实化
任务树:四维多选下拉筛选(状态/复杂度/优先级/执行者)、已选置顶、新建任务按钮移右上角(0任务也能建)。 四块主区(Agent/审批闸/任务树/归档)统一标题组件(图标+名称+数量徽标)+ 折叠按钮(localStorage 记忆)。 侧栏:全局 Agent 弹层改常驻卡片(项目默认3+折叠+按活跃排序)、顶部项目拖拽排序、全局设置移入用户菜单弹框(保存/取消,落库)、记忆当前项目。 adaptProject 用后端 summary 的 running/attention 判状态点颜色(青/琥珀/灰)+ 紫色关注数徽标;agent 标题不再越界;i18n 5 语言补全。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,63 @@
|
||||
// 任务树:筛选栏(搜索+复杂度+状态分组)+ 折叠层级 + 复杂度可点换档 + 依赖可视化跳转 + 展开详情;新建任务表单
|
||||
const KIT_FILTER_GROUPS = [
|
||||
['todo', ['init', 'ready', 'blocked']],
|
||||
['doing', ['analyzing', 'speccing', 'queued', 'executing', 'decomposed']],
|
||||
['gate', ['plan_review', 'spec_review', 'exec_review']],
|
||||
['bad', ['failed', 'needs_attention']],
|
||||
['hold', ['paused', 'cancelled']],
|
||||
['done', ['done']],
|
||||
// 状态全序(下拉选项顺序:待办 → 进行 → 容器 → 审批 → 异常 → 挂起 → 终态)
|
||||
const KIT_STATUS_ORDER = [
|
||||
'init', 'ready', 'blocked',
|
||||
'analyzing', 'speccing', 'queued', 'executing', 'decomposed',
|
||||
'plan_review', 'spec_review', 'exec_review',
|
||||
'failed', 'needs_attention',
|
||||
'paused', 'done', 'cancelled',
|
||||
];
|
||||
const KIT_CPLX_OPTS = [{ value: 'hard', label: 'HARD' }, { value: 'medium', label: 'MED' }, { value: 'easy', label: 'EASY' }];
|
||||
const KIT_PRIO_OPTS = [{ value: 'P0', label: 'P0' }, { value: 'P1', label: 'P1' }, { value: 'P2', label: 'P2' }];
|
||||
|
||||
// 主显示区各区块标题通用折叠按钮(▾ 展开 / ▸ 折叠);暴露到 window 供 Agent/审批闸/归档复用
|
||||
function KitFoldBtn({ folded, onClick, t }) {
|
||||
return (
|
||||
<button type="button" onClick={onClick} title={folded ? (t.gateExpand || '展开') : (t.gateCollapse || '折叠')} style={{
|
||||
background: 'transparent', border: '1px solid var(--line)', color: 'var(--muted)', cursor: 'pointer',
|
||||
fontSize: 10, fontFamily: 'var(--mono)', lineHeight: 1, padding: '3px 8px', borderRadius: 'var(--radius-sm,4px)',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--green)'; e.currentTarget.style.borderColor = 'var(--green-dim)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted)'; e.currentTarget.style.borderColor = 'var(--line)'; }}>
|
||||
{folded ? '▸' : '▾'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
window.MaestroKitFoldBtn = KitFoldBtn;
|
||||
|
||||
// 区块图标集(24 网格 / stroke currentColor,继承标题 accent 色)
|
||||
function KitIcon({ name }) {
|
||||
const p = { width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round', style: { flex: 'none', display: 'block' } };
|
||||
if (name === 'agent') return <svg {...p}><polyline points="3 12 7 12 10 5 14 19 17 12 21 12" /></svg>;
|
||||
if (name === 'gate') return <svg {...p}><path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6z" /><path d="M9 12l2 2 4-4" /></svg>;
|
||||
if (name === 'tasks') return <svg {...p}><path d="M9 6h11M9 12h11M9 18h11" /><path d="M4.5 6h.01M4.5 12h.01M4.5 18h.01" /></svg>;
|
||||
if (name === 'archive') return <svg {...p}><rect x="3" y="4" width="18" height="4" rx="1" /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><path d="M10 12h4" /></svg>;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 统一区块标题:图标 + 名称 + 数量徽标 + 折叠按钮(四块主显示区共用,视觉一致)
|
||||
function KitSectionHead({ icon, title, count, accent = 'var(--green)', folded, onToggleFold, t, sticky }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 9,
|
||||
fontFamily: 'var(--mono)', fontSize: 11, fontWeight: 600, letterSpacing: '.2em',
|
||||
color: 'var(--muted)', textTransform: 'uppercase', padding: '16px 0 10px',
|
||||
...(sticky ? { position: 'sticky', top: 0, zIndex: 5, background: 'linear-gradient(var(--bg) 78%, transparent)' } : {}),
|
||||
}}>
|
||||
<span style={{ color: accent, flex: 'none', display: 'inline-flex', alignItems: 'center' }}><KitIcon name={icon} /></span>
|
||||
<span style={{ whiteSpace: 'nowrap' }}>{title}</span>
|
||||
{count != null ? (
|
||||
<span style={{
|
||||
flex: 'none', fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 700, letterSpacing: '.02em',
|
||||
color: accent, background: 'var(--panel-2)', borderRadius: 10, padding: '1px 7px', lineHeight: '15px',
|
||||
minWidth: 10, textAlign: 'center',
|
||||
}}>{count}</span>
|
||||
) : null}
|
||||
<span style={{ marginLeft: 'auto', flex: 'none' }}><KitFoldBtn folded={folded} onClick={onToggleFold} t={t} /></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.MaestroKitSectionHead = KitSectionHead;
|
||||
|
||||
function kitFlatten(tasks, map = new Map()) {
|
||||
for (const tk of tasks) { map.set(tk.id, tk); if (tk.children) kitFlatten(tk.children, map); }
|
||||
@@ -53,49 +104,94 @@ function CplxPicker({ task, cplx, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FilterBar({ t, kw, setKw, cplxSet, toggleCplx, statusSet, toggleGroup, matchCount, filtering, onClear }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
const chip = (on, color, dim, label, onClick) => (
|
||||
<button key={label} onClick={onClick} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10, fontWeight: 700, letterSpacing: '.1em',
|
||||
background: on ? 'rgba(95,221,125,.08)' : 'transparent',
|
||||
color: on ? color : 'var(--faint)',
|
||||
border: '1px solid ' + (on ? dim : 'var(--line)'),
|
||||
borderRadius: 3, padding: '2px 8px', cursor: 'pointer', transition: 'all .1s', whiteSpace: 'nowrap',
|
||||
}}>{label}</button>
|
||||
);
|
||||
// 多选下拉:触发器显示「标签 ·N」,菜单内勾选切换(不随单击关闭);点外 / Esc 关闭
|
||||
function KitMultiSelect({ label, options, selected, onToggle, onClear, accent = 'var(--green)' }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const ref = React.useRef(null);
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('mousedown', onDoc);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => { document.removeEventListener('mousedown', onDoc); document.removeEventListener('keydown', onKey); };
|
||||
}, [open]);
|
||||
const n = selected.size;
|
||||
// 已选项统一排到菜单最前(打开时按「选中优先」重排,保持各自原相对顺序)
|
||||
const ordered = open
|
||||
? [...options.filter((o) => selected.has(o.value)), ...options.filter((o) => !selected.has(o.value))]
|
||||
: options;
|
||||
return (
|
||||
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px', marginBottom: 12, display: 'flex', flexDirection: 'column', gap: 7 }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<input value={kw} onChange={(e) => setKw(e.target.value)} placeholder={t.searchPh} autoComplete="off" style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 12, width: 190, padding: '4px 9px',
|
||||
background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-sm, 4px)', outline: 'none',
|
||||
}} />
|
||||
<span style={{ display: 'inline-flex', gap: 6 }}>
|
||||
{chip(cplxSet.has('hard'), 'var(--red)', 'var(--red-dim)', 'HARD', () => toggleCplx('hard'))}
|
||||
{chip(cplxSet.has('medium'), 'var(--amber)', 'var(--amber-dim)', 'MED', () => toggleCplx('medium'))}
|
||||
{chip(cplxSet.has('easy'), 'var(--green)', 'var(--green-dim)', 'EASY', () => toggleCplx('easy'))}
|
||||
<span ref={ref} style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>
|
||||
<button type="button" onClick={() => setOpen(!open)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 11.5, letterSpacing: '.03em',
|
||||
background: n ? 'rgba(95,221,125,.08)' : 'var(--bg-deep)',
|
||||
color: n ? accent : 'var(--muted)',
|
||||
border: '1px solid ' + (n ? 'var(--green-dim)' : 'var(--line)'),
|
||||
borderRadius: 'var(--radius-sm, 4px)', padding: '5px 9px', cursor: 'pointer',
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', transition: 'all .1s',
|
||||
}}>
|
||||
<span>{label}</span>
|
||||
{n ? <span style={{
|
||||
fontSize: 9.5, fontWeight: 700, background: accent, color: 'var(--bg-deep)',
|
||||
borderRadius: 8, padding: '0 5px', lineHeight: '14px', minWidth: 8, textAlign: 'center',
|
||||
}}>{n}</span> : null}
|
||||
<span style={{ fontSize: 8, color: 'var(--faint)', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .12s' }}>▼</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<span style={{
|
||||
position: 'absolute', top: 'calc(100% + 5px)', left: 0, zIndex: 80, minWidth: 152,
|
||||
display: 'grid', gap: 1, background: 'var(--bg-deep)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-md, 6px)', padding: 4, boxShadow: '0 10px 30px rgba(0,0,0,.65)',
|
||||
animation: 'maestro-rise .12s ease both', maxHeight: 320, overflowY: 'auto',
|
||||
}}>
|
||||
{n ? (
|
||||
<button type="button" onClick={onClear} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 11, textAlign: 'left', background: 'transparent',
|
||||
color: 'var(--faint)', border: 'none', borderRadius: 4, padding: '5px 8px', cursor: 'pointer',
|
||||
marginBottom: 2, borderBottom: '1px solid var(--line)',
|
||||
}}>✕ 清空</button>
|
||||
) : null}
|
||||
{ordered.map((o) => {
|
||||
const on = selected.has(o.value);
|
||||
return (
|
||||
<button type="button" key={o.value} onClick={() => onToggle(o.value)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 12, textAlign: 'left',
|
||||
background: on ? 'var(--panel-2)' : 'transparent',
|
||||
color: on ? accent : 'var(--ink)', fontWeight: on ? 700 : 400,
|
||||
border: 'none', borderRadius: 4, padding: '6px 9px 6px 7px', cursor: 'pointer',
|
||||
display: 'flex', alignItems: 'center', gap: 7, whiteSpace: 'nowrap',
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = 'var(--panel)'; }}
|
||||
onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = 'transparent'; }}>
|
||||
<span style={{ flex: 'none', width: 10, color: accent, fontSize: 10 }}>{on ? '✓' : ''}</span>{o.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
<span style={{ flex: 1 }}></span>
|
||||
{filtering ? <span style={{ fontSize: 11, color: 'var(--amber)', letterSpacing: '.08em' }}>{t.matchCount.replace('{n}', matchCount)}</span> : null}
|
||||
{filtering ? <Button variant="ghost" size="xs" onClick={onClear}>{t.clearFilter}</Button> : null}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{KIT_FILTER_GROUPS.map(([g, sts]) => {
|
||||
const on = sts.every((s) => statusSet.has(s));
|
||||
const part = !on && sts.some((s) => statusSet.has(s));
|
||||
return (
|
||||
<button key={g} onClick={() => toggleGroup(sts)} style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 10.5, letterSpacing: '.04em',
|
||||
background: 'transparent',
|
||||
color: on ? 'var(--green)' : part ? 'var(--amber)' : 'var(--muted)',
|
||||
border: '1px dashed ' + (on ? 'var(--green-dim)' : part ? 'var(--amber-dim)' : 'var(--line-soft)'),
|
||||
borderRadius: 3, padding: '2px 9px', cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
}}>{t.fgroups[g]}</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FilterBar({ t, kw, setKw, sets, toggle, clearOne, matchCount, filtering, onClear }) {
|
||||
const { Button } = window.MaestroDesignSystem_a6a290;
|
||||
const statusOpts = KIT_STATUS_ORDER.map((s) => ({ value: s, label: t.status[s] }));
|
||||
const assigneeOpts = [{ value: 'agent', label: t.assignees.agent }, { value: 'human', label: t.assignees.human }];
|
||||
return (
|
||||
<div style={{ background: 'var(--panel)', border: '1px solid var(--line)', borderRadius: 6, padding: '8px 10px', marginBottom: 12, display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<input value={kw} onChange={(e) => setKw(e.target.value)} placeholder={t.searchPh} autoComplete="off" style={{
|
||||
fontFamily: 'var(--mono)', fontSize: 12, width: 180, padding: '5px 9px',
|
||||
background: 'var(--bg-deep)', color: 'var(--ink)', border: '1px solid var(--line)',
|
||||
borderRadius: 'var(--radius-sm, 4px)', outline: 'none',
|
||||
}} />
|
||||
<KitMultiSelect label={t.fStatus} options={statusOpts} selected={sets.status} onToggle={(v) => toggle('status', v)} onClear={() => clearOne('status')} />
|
||||
<KitMultiSelect label={t.complexity} options={KIT_CPLX_OPTS} selected={sets.cplx} onToggle={(v) => toggle('cplx', v)} onClear={() => clearOne('cplx')} accent="var(--amber)" />
|
||||
<KitMultiSelect label={t.priority} options={KIT_PRIO_OPTS} selected={sets.prio} onToggle={(v) => toggle('prio', v)} onClear={() => clearOne('prio')} accent="var(--red)" />
|
||||
<KitMultiSelect label={t.fAssignee} options={assigneeOpts} selected={sets.assignee} onToggle={(v) => toggle('assignee', v)} onClear={() => clearOne('assignee')} accent="var(--violet)" />
|
||||
<span style={{ flex: 1 }}></span>
|
||||
{filtering ? <span style={{ fontSize: 11, color: 'var(--amber)', letterSpacing: '.08em' }}>{t.matchCount.replace('{n}', matchCount)}</span> : null}
|
||||
{filtering ? <Button variant="ghost" size="xs" onClick={onClear}>{t.clearFilter}</Button> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -311,13 +407,15 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
|
||||
}
|
||||
|
||||
function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const { Button, SectionHead } = window.MaestroDesignSystem_a6a290;
|
||||
const [expandedId, setExpandedId] = React.useState(null);
|
||||
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
|
||||
const [showNew, setShowNew] = React.useState(false);
|
||||
const [folded, setFolded] = React.useState(() => localStorage.getItem('maestro-kit-fold-tasks') === '1');
|
||||
const toggleFold = () => setFolded((f) => { localStorage.setItem('maestro-kit-fold-tasks', f ? '0' : '1'); return !f; });
|
||||
const [kw, setKw] = React.useState('');
|
||||
const [cplxSet, setCplxSet] = React.useState(() => new Set());
|
||||
const [statusSet, setStatusSet] = React.useState(() => new Set());
|
||||
const [cplxSet, setCplxSet] = React.useState(() => new Set());
|
||||
const [prioSet, setPrioSet] = React.useState(() => new Set());
|
||||
const [assigneeSet, setAssigneeSet] = React.useState(() => new Set());
|
||||
const [cplxOverride, setCplxOverride] = React.useState({});
|
||||
const [flashId, setFlashId] = React.useState(null);
|
||||
const byId = React.useMemo(() => kitFlatten(tasks), [tasks]);
|
||||
@@ -329,25 +427,25 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const toggleOpen = (id) => setOpenIds((prev) => {
|
||||
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
|
||||
});
|
||||
const toggleCplx = (v) => setCplxSet((prev) => {
|
||||
// 四维多选筛选:status / cplx / prio / assignee
|
||||
const setters = { status: setStatusSet, cplx: setCplxSet, prio: setPrioSet, assignee: setAssigneeSet };
|
||||
const sets = { status: statusSet, cplx: cplxSet, prio: prioSet, assignee: assigneeSet };
|
||||
const toggle = (key, v) => setters[key]((prev) => {
|
||||
const next = new Set(prev); next.has(v) ? next.delete(v) : next.add(v); return next;
|
||||
});
|
||||
const toggleGroup = (sts) => setStatusSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
const allOn = sts.every((s) => next.has(s));
|
||||
for (const s of sts) allOn ? next.delete(s) : next.add(s);
|
||||
return next;
|
||||
});
|
||||
const filtering = kw.trim() !== '' || cplxSet.size > 0 || statusSet.size > 0;
|
||||
const clearFilters = () => { setKw(''); setCplxSet(new Set()); setStatusSet(new Set()); };
|
||||
const clearOne = (key) => setters[key](new Set());
|
||||
const filtering = kw.trim() !== '' || statusSet.size > 0 || cplxSet.size > 0 || prioSet.size > 0 || assigneeSet.size > 0;
|
||||
const clearFilters = () => { setKw(''); setStatusSet(new Set()); setCplxSet(new Set()); setPrioSet(new Set()); setAssigneeSet(new Set()); };
|
||||
|
||||
// 筛选:自身命中 → 显示;祖先链作为上下文淡显;命中节点的父级自动展开
|
||||
const { visibleSet, matchCount } = React.useMemo(() => {
|
||||
if (!filtering) return { visibleSet: null, matchCount: 0 };
|
||||
const matches = (tk) =>
|
||||
(kw.trim() === '' || tk.title.toLowerCase().includes(kw.trim().toLowerCase())) &&
|
||||
(statusSet.size === 0 || statusSet.has(tk.status)) &&
|
||||
(cplxSet.size === 0 || cplxSet.has(cplxOverride[tk.id] || tk.cplx)) &&
|
||||
(statusSet.size === 0 || statusSet.has(tk.status));
|
||||
(prioSet.size === 0 || prioSet.has(tk.prio)) &&
|
||||
(assigneeSet.size === 0 || assigneeSet.has((tk._raw && tk._raw.assignee) || ''));
|
||||
const vis = new Set(); const ctx = new Set(); let count = 0;
|
||||
const walk = (tk) => {
|
||||
let childHit = false;
|
||||
@@ -360,7 +458,7 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
for (const tk of tasks) walk(tk);
|
||||
const set = new Set(vis); set.ctx = ctx;
|
||||
return { visibleSet: set, matchCount: count };
|
||||
}, [filtering, kw, cplxSet, statusSet, tasks, cplxOverride]);
|
||||
}, [filtering, kw, statusSet, cplxSet, prioSet, assigneeSet, tasks, cplxOverride]);
|
||||
|
||||
// 依赖跳转:展开所有祖先 + flash 定位
|
||||
const onJump = (id) => {
|
||||
@@ -384,20 +482,22 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
|
||||
const roots = visibleSet ? tasks.filter((tk) => visibleSet.has(tk.id)) : tasks;
|
||||
return (
|
||||
<section>
|
||||
<SectionHead title={t.taskSection} sticky action={<Button variant="ghost" size="xs" onClick={() => setShowNew(!showNew)}>{t.newTask}</Button>} />
|
||||
{showNew ? <NewTaskPanel tasks={tasks} t={t} onCancel={() => setShowNew(false)}
|
||||
onCreate={(payload) => { setShowNew(false); onCreate ? onCreate(payload) : onToast('ok', t.toastCreated); }} /> : null}
|
||||
<FilterBar t={t} kw={kw} setKw={setKw} cplxSet={cplxSet} toggleCplx={toggleCplx}
|
||||
statusSet={statusSet} toggleGroup={toggleGroup} matchCount={matchCount} filtering={filtering} onClear={clearFilters} />
|
||||
<div>
|
||||
{roots.map((tk) => (
|
||||
<TaskRow key={tk.id} task={tk} depth={0} ctx={visibleSet ? visibleSet.ctx.has(tk.id) : false}
|
||||
expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
|
||||
byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} />
|
||||
))}
|
||||
</div>
|
||||
<KitSectionHead icon="tasks" title={t.taskSection} count={byId.size} accent="var(--green)" folded={folded} onToggleFold={toggleFold} t={t} sticky />
|
||||
{!folded ? (
|
||||
<React.Fragment>
|
||||
<FilterBar t={t} kw={kw} setKw={setKw} sets={sets} toggle={toggle} clearOne={clearOne}
|
||||
matchCount={matchCount} filtering={filtering} onClear={clearFilters} />
|
||||
<div>
|
||||
{roots.map((tk) => (
|
||||
<TaskRow key={tk.id} task={tk} depth={0} ctx={visibleSet ? visibleSet.ctx.has(tk.id) : false}
|
||||
expandedId={expandedId} setExpandedId={setExpandedId} openIds={openIds} toggleOpen={toggleOpen} t={t}
|
||||
byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} />
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, { MaestroKitTaskSection: TaskSection });
|
||||
Object.assign(window, { MaestroKitTaskSection: TaskSection, MaestroKitNewTaskPanel: NewTaskPanel });
|
||||
|
||||
Reference in New Issue
Block a user