// Backlog tree view: rows, expand/collapse, status cycling, drag handle, add/delete. const { useState, useRef, useEffect, useMemo } = React; // Inline priority popover function PriorityPicker({ value, onChange, onClose }) { const ref = useRef(null); useEffect(() => { const close = (e) => { if (!ref.current?.contains(e.target)) onClose(); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, [onClose]); return (
e.stopPropagation()}> {PRIORITIES.map(p => ( ))}
); } // Inline status popover (replaces ●●● menu) function StatusPopover({ value, style, onChange, onClose, anchorRect }) { const ref = useRef(null); useEffect(() => { const close = (e) => { if (!ref.current?.contains(e.target)) onClose(); }; document.addEventListener("mousedown", close); return () => document.removeEventListener("mousedown", close); }, [onClose]); return (
e.stopPropagation()}> {STATUSES.map(s => ( ))}
); } // Single tree row function ItemRow({ item, depth, expanded, hasChildren, statusStyle, onToggle, onSetStatus, onAddChild, onDelete, onEdit, onSetPriority, onMoveUp, onMoveDown, canMoveUp, canMoveDown, onDragStart, onDragOver, onDrop, dimmed, query, dropTarget }) { const [statusOpen, setStatusOpen] = useState(false); const [priOpen, setPriOpen] = useState(false); const overdue = isOverdue(item); const isP0 = item.priority === "P0" && item.status !== "done" && item.status !== "cancelled"; const isDone = item.status === "done" || item.status === "cancelled"; const renderTitle = () => { if (!query) return item.title; const i = item.title.toLowerCase().indexOf(query.toLowerCase()); if (i < 0) return item.title; return (<> {item.title.slice(0, i)} {item.title.slice(i, i + query.length)} {item.title.slice(i + query.length)} ); }; return (
onDragStart?.(e, item)} onDragOver={(e) => onDragOver?.(e, item)} onDrop={(e) => onDrop?.(e, item)} > {statusOpen && ( setStatusOpen(false)}/> )} {priOpen && ( onSetPriority(p)} onClose={() => setPriOpen(false)}/> )} {renderTitle()} {shouldShowProgress(item) && ( )} {item.due && ( {dueRelative(item)} )} {item.tags?.length > 0 && ( {item.tags.map(t => #{t})} )} {item.status === "blocked" && item.reason && ( ↳ {item.reason} )} {depth < 3 && ( )}
); } // Expanded-state map kept in parent so filtering doesn't lose it. function BacklogTree({ items, expandedMap, setExpanded, onMutate, query, statusStyle, manualOrder }) { const [draggedId, setDraggedId] = useState(null); const [dropTargetId, setDropTargetId] = useState(null); const onDragStart = (e, item) => { setDraggedId(item.id); e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", item.id); } catch (_) {} }; const onDragOver = (e, item) => { if (!draggedId || draggedId === item.id) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDropTargetId(item.id); }; const onDrop = (e, item) => { e.preventDefault(); if (draggedId && draggedId !== item.id) { onMutate.reorder(draggedId, item.id); } setDraggedId(null); setDropTargetId(null); }; function renderList(list, depth, parentList) { return list.map((item, idx) => { const expanded = expandedMap[item.id] ?? !item.collapsed; const hasChildren = item.children?.length > 0; const forceOpen = query && item._matches !== true && hasChildren; const showKids = (expanded || forceOpen) && hasChildren; // can-move logic: only swap with adjacent same-priority sibling const samePri = parentList .map((x, i) => ({ x, i })) .filter(({ x }) => x.priority === item.priority); const myIdx = samePri.findIndex(({ x }) => x.id === item.id); const canMoveUp = myIdx > 0; const canMoveDown = myIdx >= 0 && myIdx < samePri.length - 1; return ( setExpanded(item.id, !expanded)} onSetStatus={(s) => onMutate.setStatus(item.id, s)} onSetPriority={(p) => onMutate.setPriority(item.id, p)} onAddChild={() => onMutate.addChild(item.id)} onDelete={() => onMutate.deleteItem(item.id)} onEdit={() => onMutate.editItem(item.id)} onMoveUp={() => onMutate.moveWithinPriority(item.id, -1)} onMoveDown={() => onMutate.moveWithinPriority(item.id, +1)} canMoveUp={canMoveUp} canMoveDown={canMoveDown} onDragStart={onDragStart} onDragOver={onDragOver} onDrop={onDrop} /> {showKids && renderList(item.children, depth + 1, item.children)} ); }); } return
{renderList(items, 0, items)}
; } window.BacklogTree = BacklogTree;