Replace project filter buttons with multi-select dropdown
Projects now appear as a dropdown with checkboxes instead of horizontal buttons. Supports All Projects, Clear All, and individual project toggle filtering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19f669f7d1
commit
f30d3d603c
|
|
@ -38,7 +38,9 @@ function App() {
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
||||||
const [filter, setFilter] = useState<string>("");
|
const [filter, setFilter] = useState<string>("");
|
||||||
const [selectedProject, setSelectedProject] = useState<string | null>(null);
|
const [selectedProjects, setSelectedProjects] = useState<Set<string> | null>(null); // null = all
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
const [draggedTask, setDraggedTask] = useState<AggregatedTask | null>(null);
|
const [draggedTask, setDraggedTask] = useState<AggregatedTask | null>(null);
|
||||||
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
|
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null);
|
||||||
|
|
@ -205,13 +207,52 @@ function App() {
|
||||||
};
|
};
|
||||||
}, [connectWebSocket]);
|
}, [connectWebSocket]);
|
||||||
|
|
||||||
|
// Close dropdown on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("click", handleClick);
|
||||||
|
return () => document.removeEventListener("click", handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isProjectSelected = (name: string) => selectedProjects === null || selectedProjects.has(name);
|
||||||
|
|
||||||
|
const toggleProject = (name: string) => {
|
||||||
|
const activeNames = projects.filter((p) => tasks.some((t) => t.projectName === p.name)).map((p) => p.name);
|
||||||
|
if (selectedProjects === null) {
|
||||||
|
const next = new Set(activeNames);
|
||||||
|
next.delete(name);
|
||||||
|
setSelectedProjects(next);
|
||||||
|
} else if (selectedProjects.has(name)) {
|
||||||
|
const next = new Set(selectedProjects);
|
||||||
|
next.delete(name);
|
||||||
|
setSelectedProjects(next);
|
||||||
|
} else {
|
||||||
|
const next = new Set(selectedProjects);
|
||||||
|
next.add(name);
|
||||||
|
setSelectedProjects(next.size === activeNames.length ? null : next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeProjects = projects.filter((p) => tasks.some((t) => t.projectName === p.name));
|
||||||
|
|
||||||
|
const dropdownLabel = (() => {
|
||||||
|
if (selectedProjects === null) return "All Projects";
|
||||||
|
if (selectedProjects.size === 0) return "No Projects";
|
||||||
|
if (selectedProjects.size === 1) return [...selectedProjects][0];
|
||||||
|
return `${selectedProjects.size} Projects`;
|
||||||
|
})();
|
||||||
|
|
||||||
// Group tasks by status
|
// Group tasks by status
|
||||||
const statuses = ["To Do", "In Progress", "Done", "Won't Do"];
|
const statuses = ["To Do", "In Progress", "Done", "Won't Do"];
|
||||||
const tasksByStatus = statuses.reduce(
|
const tasksByStatus = statuses.reduce(
|
||||||
(acc, status) => {
|
(acc, status) => {
|
||||||
acc[status] = tasks.filter((t) => {
|
acc[status] = tasks.filter((t) => {
|
||||||
const statusMatch = t.status.toLowerCase() === status.toLowerCase();
|
const statusMatch = t.status.toLowerCase() === status.toLowerCase();
|
||||||
const projectMatch = !selectedProject || t.projectName === selectedProject;
|
const projectMatch = selectedProjects === null || selectedProjects.has(t.projectName);
|
||||||
const filterMatch =
|
const filterMatch =
|
||||||
!filter ||
|
!filter ||
|
||||||
t.title.toLowerCase().includes(filter.toLowerCase()) ||
|
t.title.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
|
|
@ -348,46 +389,150 @@ function App() {
|
||||||
minWidth: "200px",
|
minWidth: "200px",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<div ref={dropdownRef} style={{ position: "relative" }}>
|
||||||
onClick={() => setSelectedProject(null)}
|
<div
|
||||||
style={{
|
onClick={(e) => {
|
||||||
padding: "0.5rem 1rem",
|
e.stopPropagation();
|
||||||
borderRadius: "0.375rem",
|
setDropdownOpen(!dropdownOpen);
|
||||||
border: selectedProject === null ? "2px solid #3b82f6" : "1px solid #334155",
|
}}
|
||||||
backgroundColor: selectedProject === null ? "#1e40af" : "#0f172a",
|
|
||||||
color: "#e2e8f0",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
All Projects
|
|
||||||
</button>
|
|
||||||
{projects.map((project) => (
|
|
||||||
<button
|
|
||||||
key={project.path}
|
|
||||||
onClick={() => setSelectedProject(project.name)}
|
|
||||||
style={{
|
style={{
|
||||||
padding: "0.5rem 1rem",
|
padding: "0.5rem 1rem",
|
||||||
borderRadius: "0.375rem",
|
borderRadius: "0.375rem",
|
||||||
border: selectedProject === project.name ? `2px solid ${project.color}` : "1px solid #334155",
|
border: "1px solid #334155",
|
||||||
backgroundColor: selectedProject === project.name ? project.color + "33" : "#0f172a",
|
backgroundColor: "#0f172a",
|
||||||
color: "#e2e8f0",
|
color: "#e2e8f0",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
minWidth: "180px",
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span style={{ display: "flex", gap: "0.25rem", alignItems: "center" }}>
|
||||||
|
{selectedProjects !== null && selectedProjects.size > 0 && selectedProjects.size <= 5
|
||||||
|
? [...selectedProjects].map((name) => {
|
||||||
|
const proj = projects.find((p) => p.name === name);
|
||||||
|
return proj ? (
|
||||||
|
<span
|
||||||
|
key={name}
|
||||||
|
style={{
|
||||||
|
width: selectedProjects.size === 1 ? "0.75rem" : "0.5rem",
|
||||||
|
height: selectedProjects.size === 1 ? "0.75rem" : "0.5rem",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: proj.color,
|
||||||
|
display: "inline-block",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</span>
|
||||||
|
<span>{dropdownLabel}</span>
|
||||||
|
<span style={{ marginLeft: "auto", fontSize: "0.625rem", transition: "transform 0.2s", transform: dropdownOpen ? "rotate(180deg)" : "none" }}>
|
||||||
|
▼
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: "0.75rem",
|
position: "absolute",
|
||||||
height: "0.75rem",
|
top: "calc(100% + 4px)",
|
||||||
borderRadius: "50%",
|
left: 0,
|
||||||
backgroundColor: project.color,
|
minWidth: "220px",
|
||||||
|
background: "#1e293b",
|
||||||
|
border: "1px solid #334155",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
boxShadow: "0 10px 25px rgba(0,0,0,0.4)",
|
||||||
|
zIndex: 100,
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
{project.name}
|
<div style={{ display: "flex", borderBottom: "1px solid #334155" }}>
|
||||||
</button>
|
<button
|
||||||
))}
|
onClick={() => setSelectedProjects(null)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: "0.5rem",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
borderRight: "1px solid #334155",
|
||||||
|
color: "#94a3b8",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All Projects
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedProjects(new Set())}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: "0.5rem",
|
||||||
|
background: "none",
|
||||||
|
border: "none",
|
||||||
|
color: "#94a3b8",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{activeProjects.map((project) => {
|
||||||
|
const selected = isProjectSelected(project.name);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={project.path}
|
||||||
|
onClick={() => toggleProject(project.name)}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.5rem",
|
||||||
|
padding: "0.5rem 0.75rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.currentTarget.style.background = "#334155")}
|
||||||
|
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "1rem",
|
||||||
|
height: "1rem",
|
||||||
|
border: selected ? "1.5px solid #3b82f6" : "1.5px solid #475569",
|
||||||
|
borderRadius: "3px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
background: selected ? "#3b82f6" : "transparent",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selected && (
|
||||||
|
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||||
|
<path d="M2 5L4 7L8 3" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: "0.75rem",
|
||||||
|
height: "0.75rem",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: project.color,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{project.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* View Content */}
|
{/* View Content */}
|
||||||
|
|
@ -486,7 +631,7 @@ function App() {
|
||||||
<DependencyGraph
|
<DependencyGraph
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
selectedProject={selectedProject}
|
selectedProjects={selectedProjects}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
showCompleted={showCompleted}
|
showCompleted={showCompleted}
|
||||||
/>
|
/>
|
||||||
|
|
@ -842,7 +987,7 @@ function TaskCard({ task, onArchive, onDelete, onUpdate, statuses, onDragStart,
|
||||||
interface DependencyGraphProps {
|
interface DependencyGraphProps {
|
||||||
tasks: AggregatedTask[];
|
tasks: AggregatedTask[];
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
selectedProject: string | null;
|
selectedProjects: Set<string> | null;
|
||||||
filter: string;
|
filter: string;
|
||||||
showCompleted: boolean;
|
showCompleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -873,7 +1018,7 @@ function isCompleted(status: string): boolean {
|
||||||
return lower.includes("done") || lower.includes("complete");
|
return lower.includes("done") || lower.includes("complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
function DependencyGraph({ tasks, projects, selectedProject, filter, showCompleted }: DependencyGraphProps) {
|
function DependencyGraph({ tasks, projects, selectedProjects, filter, showCompleted }: DependencyGraphProps) {
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
|
||||||
|
|
@ -882,11 +1027,11 @@ function DependencyGraph({ tasks, projects, selectedProject, filter, showComplet
|
||||||
const filteredTasks = useMemo(() => {
|
const filteredTasks = useMemo(() => {
|
||||||
return tasks.filter((t) => {
|
return tasks.filter((t) => {
|
||||||
if (!showCompleted && isCompleted(t.status)) return false;
|
if (!showCompleted && isCompleted(t.status)) return false;
|
||||||
if (selectedProject && t.projectName !== selectedProject) return false;
|
if (selectedProjects !== null && !selectedProjects.has(t.projectName)) return false;
|
||||||
if (filter && !t.title.toLowerCase().includes(filter.toLowerCase())) return false;
|
if (filter && !t.title.toLowerCase().includes(filter.toLowerCase())) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [tasks, showCompleted, selectedProject, filter]);
|
}, [tasks, showCompleted, selectedProjects, filter]);
|
||||||
|
|
||||||
// Build graph data - note: aggregated tasks don't have dependencies in YAML
|
// Build graph data - note: aggregated tasks don't have dependencies in YAML
|
||||||
// So we'll just show all tasks as nodes without edges for now
|
// So we'll just show all tasks as nodes without edges for now
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,34 @@
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
.project-btn, .action-btn {
|
.action-btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
background: #22c55e;
|
||||||
|
border-color: #16a34a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.action-btn:hover { background: #16a34a; }
|
||||||
|
.project-dot {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Project Dropdown Filter */
|
||||||
|
.project-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.project-dropdown-trigger {
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
|
|
@ -59,14 +86,97 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
min-width: 180px;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.project-btn.active { border-width: 2px; border-color: #3b82f6; background: #1e40af; }
|
.project-dropdown-trigger:hover {
|
||||||
.action-btn { background: #22c55e; border-color: #16a34a; font-weight: 500; }
|
border-color: #475569;
|
||||||
.action-btn:hover { background: #16a34a; }
|
background: #1e293b;
|
||||||
.project-dot {
|
}
|
||||||
width: 0.75rem;
|
.project-dropdown-trigger .arrow {
|
||||||
height: 0.75rem;
|
margin-left: auto;
|
||||||
border-radius: 50%;
|
font-size: 0.625rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.project-dropdown.open .arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.project-dropdown-trigger .selected-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.project-dropdown-menu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.4);
|
||||||
|
z-index: 100;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.project-dropdown.open .project-dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.project-dropdown-actions {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.project-dropdown-actions button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.project-dropdown-actions button:hover {
|
||||||
|
background: #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.project-dropdown-actions button:first-child {
|
||||||
|
border-right: 1px solid #334155;
|
||||||
|
}
|
||||||
|
.project-dropdown-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.project-dropdown-item:hover {
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
.project-dropdown-item .checkbox {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 1.5px solid #475569;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
.project-dropdown-item.selected .checkbox {
|
||||||
|
background: #3b82f6;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
.project-dropdown-item.selected .checkbox::after {
|
||||||
|
content: '';
|
||||||
|
width: 0.35rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg) translateY(-1px);
|
||||||
}
|
}
|
||||||
.board {
|
.board {
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
|
|
@ -516,7 +626,20 @@
|
||||||
|
|
||||||
<div class="filter-bar" id="filter-bar">
|
<div class="filter-bar" id="filter-bar">
|
||||||
<input type="text" id="filter" class="filter-input" placeholder="Filter tasks...">
|
<input type="text" id="filter" class="filter-input" placeholder="Filter tasks...">
|
||||||
<button class="project-btn active" id="all-projects">All Projects</button>
|
<div class="project-dropdown" id="project-dropdown">
|
||||||
|
<div class="project-dropdown-trigger" id="project-dropdown-trigger">
|
||||||
|
<span class="selected-dots" id="selected-dots"></span>
|
||||||
|
<span id="dropdown-label">All Projects</span>
|
||||||
|
<span class="arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-dropdown-menu" id="project-dropdown-menu">
|
||||||
|
<div class="project-dropdown-actions">
|
||||||
|
<button id="select-all-projects">All Projects</button>
|
||||||
|
<button id="clear-all-projects">Clear All</button>
|
||||||
|
</div>
|
||||||
|
<div id="project-dropdown-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="stats-toggle" id="stats-toggle" style="margin-left: auto;">📊 Velocity</button>
|
<button class="stats-toggle" id="stats-toggle" style="margin-left: auto;">📊 Velocity</button>
|
||||||
<button class="action-btn" id="new-task-btn">+ New Task</button>
|
<button class="action-btn" id="new-task-btn">+ New Task</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -660,7 +783,7 @@
|
||||||
<script>
|
<script>
|
||||||
let projects = [];
|
let projects = [];
|
||||||
let tasks = [];
|
let tasks = [];
|
||||||
let selectedProject = null;
|
let selectedProjects = null; // null = all projects, Set = specific projects
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let draggedTask = null;
|
let draggedTask = null;
|
||||||
let currentView = 'kanban';
|
let currentView = 'kanban';
|
||||||
|
|
@ -775,26 +898,107 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderProjects() {
|
function renderProjects() {
|
||||||
const filterBar = document.getElementById('filter-bar');
|
const list = document.getElementById('project-dropdown-list');
|
||||||
const oldBtns = filterBar.querySelectorAll('.project-btn:not(#all-projects)');
|
list.innerHTML = '';
|
||||||
oldBtns.forEach(btn => btn.remove());
|
|
||||||
|
|
||||||
const newTaskBtn = document.getElementById('new-task-btn');
|
const activeProjects = projects.filter(p => tasks.some(t => t.projectName === p.name));
|
||||||
projects.filter(p => tasks.some(t => t.projectName === p.name)).forEach(p => {
|
activeProjects.forEach(p => {
|
||||||
const btn = document.createElement('button');
|
const item = document.createElement('div');
|
||||||
btn.className = 'project-btn';
|
item.className = 'project-dropdown-item' + (isProjectSelected(p.name) ? ' selected' : '');
|
||||||
btn.innerHTML = `<span class="project-dot" style="background:${p.color}"></span>${p.name}`;
|
item.dataset.project = p.name;
|
||||||
btn.onclick = () => {
|
item.innerHTML = `<span class="checkbox"></span><span class="project-dot" style="background:${p.color}"></span>${p.name}`;
|
||||||
selectedProject = p.name;
|
item.onclick = (e) => {
|
||||||
document.querySelectorAll('.project-btn').forEach(b => b.classList.remove('active'));
|
e.stopPropagation();
|
||||||
btn.classList.add('active');
|
toggleProject(p.name);
|
||||||
renderTasks();
|
|
||||||
};
|
};
|
||||||
filterBar.insertBefore(btn, newTaskBtn);
|
list.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateDropdownLabel();
|
||||||
|
|
||||||
document.getElementById('stats').textContent =
|
document.getElementById('stats').textContent =
|
||||||
`${projects.filter(p => tasks.some(t => t.projectName === p.name)).length} active projects | ${tasks.length} tasks | Updated ${new Date().toLocaleTimeString()}`;
|
`${activeProjects.length} active projects | ${tasks.length} tasks | Updated ${new Date().toLocaleTimeString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isProjectSelected(name) {
|
||||||
|
if (selectedProjects === null) return true;
|
||||||
|
return selectedProjects.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleProject(name) {
|
||||||
|
const activeProjects = projects.filter(p => tasks.some(t => t.projectName === p.name));
|
||||||
|
// If currently "all", switch to all-except-this
|
||||||
|
if (selectedProjects === null) {
|
||||||
|
selectedProjects = new Set(activeProjects.map(p => p.name));
|
||||||
|
selectedProjects.delete(name);
|
||||||
|
} else if (selectedProjects.has(name)) {
|
||||||
|
selectedProjects.delete(name);
|
||||||
|
// If none selected, keep as empty set (shows nothing)
|
||||||
|
} else {
|
||||||
|
selectedProjects.add(name);
|
||||||
|
// If all now selected, switch back to null (all)
|
||||||
|
if (selectedProjects.size === activeProjects.length) {
|
||||||
|
selectedProjects = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderProjects();
|
||||||
|
renderTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllProjects() {
|
||||||
|
selectedProjects = null;
|
||||||
|
renderProjects();
|
||||||
|
renderTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllProjects() {
|
||||||
|
selectedProjects = new Set();
|
||||||
|
renderProjects();
|
||||||
|
renderTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDropdownLabel() {
|
||||||
|
const label = document.getElementById('dropdown-label');
|
||||||
|
const dotsContainer = document.getElementById('selected-dots');
|
||||||
|
dotsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const activeProjects = projects.filter(p => tasks.some(t => t.projectName === p.name));
|
||||||
|
|
||||||
|
if (selectedProjects === null || selectedProjects.size === activeProjects.length) {
|
||||||
|
label.textContent = 'All Projects';
|
||||||
|
} else if (selectedProjects.size === 0) {
|
||||||
|
label.textContent = 'No Projects';
|
||||||
|
} else if (selectedProjects.size === 1) {
|
||||||
|
const name = [...selectedProjects][0];
|
||||||
|
const proj = projects.find(p => p.name === name);
|
||||||
|
if (proj) {
|
||||||
|
dotsContainer.innerHTML = `<span class="project-dot" style="background:${proj.color}"></span>`;
|
||||||
|
}
|
||||||
|
label.textContent = name;
|
||||||
|
} else {
|
||||||
|
// Show colored dots for selected projects
|
||||||
|
[...selectedProjects].forEach(name => {
|
||||||
|
const proj = projects.find(p => p.name === name);
|
||||||
|
if (proj) {
|
||||||
|
const dot = document.createElement('span');
|
||||||
|
dot.className = 'project-dot';
|
||||||
|
dot.style.background = proj.color;
|
||||||
|
dot.style.width = '0.5rem';
|
||||||
|
dot.style.height = '0.5rem';
|
||||||
|
dotsContainer.appendChild(dot);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
label.textContent = `${selectedProjects.size} Projects`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update checkbox states
|
||||||
|
document.querySelectorAll('.project-dropdown-item').forEach(item => {
|
||||||
|
if (isProjectSelected(item.dataset.project)) {
|
||||||
|
item.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
item.classList.remove('selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function archiveTask(projectPath, taskId) {
|
async function archiveTask(projectPath, taskId) {
|
||||||
|
|
@ -931,7 +1135,7 @@
|
||||||
let counts = { todo: 0, progress: 0, done: 0, wontdo: 0 };
|
let counts = { todo: 0, progress: 0, done: 0, wontdo: 0 };
|
||||||
|
|
||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
if (selectedProject && task.projectName !== selectedProject) return;
|
if (selectedProjects !== null && !selectedProjects.has(task.projectName)) return;
|
||||||
if (filter && !task.title.toLowerCase().includes(filter) &&
|
if (filter && !task.title.toLowerCase().includes(filter) &&
|
||||||
!task.projectName.toLowerCase().includes(filter)) return;
|
!task.projectName.toLowerCase().includes(filter)) return;
|
||||||
|
|
||||||
|
|
@ -1199,11 +1403,29 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('filter').addEventListener('input', renderTasks);
|
document.getElementById('filter').addEventListener('input', renderTasks);
|
||||||
document.getElementById('all-projects').onclick = () => {
|
|
||||||
selectedProject = null;
|
// Project dropdown toggle
|
||||||
document.querySelectorAll('.project-btn').forEach(b => b.classList.remove('active'));
|
document.getElementById('project-dropdown-trigger').onclick = (e) => {
|
||||||
document.getElementById('all-projects').classList.add('active');
|
e.stopPropagation();
|
||||||
renderTasks();
|
document.getElementById('project-dropdown').classList.toggle('open');
|
||||||
|
};
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const dd = document.getElementById('project-dropdown');
|
||||||
|
if (!dd.contains(e.target)) {
|
||||||
|
dd.classList.remove('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Prevent dropdown menu clicks from closing
|
||||||
|
document.getElementById('project-dropdown-menu').onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
// All / Clear buttons
|
||||||
|
document.getElementById('select-all-projects').onclick = () => {
|
||||||
|
selectAllProjects();
|
||||||
|
};
|
||||||
|
document.getElementById('clear-all-projects').onclick = () => {
|
||||||
|
clearAllProjects();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Velocity Statistics Toggle
|
// Velocity Statistics Toggle
|
||||||
|
|
@ -1244,8 +1466,8 @@
|
||||||
let timesToStart = [];
|
let timesToStart = [];
|
||||||
let weeklyCompletions = [0, 0, 0, 0]; // Last 4 weeks
|
let weeklyCompletions = [0, 0, 0, 0]; // Last 4 weeks
|
||||||
|
|
||||||
const filteredTasks = selectedProject
|
const filteredTasks = selectedProjects !== null
|
||||||
? tasks.filter(t => t.projectName === selectedProject)
|
? tasks.filter(t => selectedProjects.has(t.projectName))
|
||||||
: tasks;
|
: tasks;
|
||||||
|
|
||||||
filteredTasks.forEach(task => {
|
filteredTasks.forEach(task => {
|
||||||
|
|
@ -1370,8 +1592,8 @@
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// Filter tasks - show ALL tasks, not just those with dependencies
|
// Filter tasks - show ALL tasks, not just those with dependencies
|
||||||
let filteredTasks = selectedProject
|
let filteredTasks = selectedProjects !== null
|
||||||
? tasks.filter(t => t.projectName === selectedProject)
|
? tasks.filter(t => selectedProjects.has(t.projectName))
|
||||||
: tasks;
|
: tasks;
|
||||||
|
|
||||||
if (!showCompleted) {
|
if (!showCompleted) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue