feat(board): the No Milestone group collapses, and every group's Done column starts folded (#4077)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 35s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 35s
Operator, 2026-09-15: the unmilestoned group should collapse, "especially the done column as it currently consume a lot of vertical space for [work] that's already done." - The "No Milestone" group gets the milestones' chevron and collapse, keyed as 0 in the same Set (no milestone id is 0), and starts collapsed on first load when everything in it is finished — the rule finished milestones already follow. - Each group's Done column header becomes a button that folds its cards, with the count still showing and aria-expanded set. Folded by default: done work is the part of a board nobody is reading. - Not persisted, matching the milestone collapse beside it. Milestone 415 step 2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -179,6 +179,25 @@ const milestoneGroups = computed((): MilestoneGroup[] => {
|
||||
return groups;
|
||||
});
|
||||
|
||||
// The "No Milestone" group has no id, and collapsing it uses the same Set as
|
||||
// the milestones: 0 stands for it, since no milestone id is ever 0.
|
||||
const UNASSIGNED_KEY = 0;
|
||||
function groupKey(group: MilestoneGroup): number {
|
||||
return group.milestone?.id ?? UNASSIGNED_KEY;
|
||||
}
|
||||
|
||||
// A group's Done column starts COLLAPSED, to its header and count. Done work is
|
||||
// the part of a board nobody is reading, and listed in full it pushed the open
|
||||
// columns of every group below it off the screen (operator, 2026-09-15). Not
|
||||
// persisted, matching the milestone collapse beside it: each load starts from
|
||||
// the same defaults.
|
||||
const expandedDone = ref<Set<number>>(new Set());
|
||||
function toggleDone(group: MilestoneGroup) {
|
||||
const key = groupKey(group);
|
||||
if (expandedDone.value.has(key)) expandedDone.value.delete(key);
|
||||
else expandedDone.value.add(key);
|
||||
}
|
||||
|
||||
function toggleMilestoneCollapse(id: number) {
|
||||
if (collapsedMilestones.value.has(id)) {
|
||||
collapsedMilestones.value.delete(id);
|
||||
@@ -386,6 +405,16 @@ async function loadTasks() {
|
||||
all.push(...next.notes);
|
||||
}
|
||||
tasks.value = all;
|
||||
// The No Milestone group follows the milestone rule: once, on first load,
|
||||
// it starts collapsed when everything in it is finished.
|
||||
if (!autoCollapsedOnce.value.has(UNASSIGNED_KEY)) {
|
||||
autoCollapsedOnce.value.add(UNASSIGNED_KEY);
|
||||
const assigned = new Set(milestones.value.map((m) => m.id));
|
||||
const loose = all.filter((t) => !t.milestone_id || !assigned.has(t.milestone_id));
|
||||
if (loose.length && loose.every((t) => t.status === "done" || t.status === "cancelled")) {
|
||||
collapsedMilestones.value.add(UNASSIGNED_KEY);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Say so. This used to swallow the error and leave an empty board, which is
|
||||
// indistinguishable from a project with no tasks — the same "hidden with no
|
||||
@@ -950,11 +979,12 @@ async function confirmDelete() {
|
||||
<div v-for="group in milestoneGroups" :key="group.milestone?.id ?? 'unassigned'" class="milestone-group">
|
||||
<div
|
||||
class="milestone-header"
|
||||
:class="{ clickable: !!group.milestone && renamingMilestoneId !== group.milestone?.id }"
|
||||
@click="group.milestone && renamingMilestoneId !== group.milestone.id && toggleMilestoneCollapse(group.milestone.id)"
|
||||
:class="{ clickable: renamingMilestoneId !== group.milestone?.id }"
|
||||
:aria-expanded="!collapsedMilestones.has(groupKey(group))"
|
||||
@click="renamingMilestoneId !== group.milestone?.id && toggleMilestoneCollapse(groupKey(group))"
|
||||
>
|
||||
<span class="ms-chevron" v-if="group.milestone">
|
||||
<ChevronRight v-if="collapsedMilestones.has(group.milestone.id)" :size="16" />
|
||||
<span class="ms-chevron">
|
||||
<ChevronRight v-if="collapsedMilestones.has(groupKey(group))" :size="16" />
|
||||
<ChevronDown v-else :size="16" />
|
||||
</span>
|
||||
<template v-if="group.milestone && renamingMilestoneId === group.milestone.id">
|
||||
@@ -1023,7 +1053,7 @@ async function confirmDelete() {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="!group.milestone || !collapsedMilestones.has(group.milestone.id)" class="kanban">
|
||||
<div v-if="!collapsedMilestones.has(groupKey(group))" class="kanban">
|
||||
<!-- Todo column -->
|
||||
<div class="kanban-col col-todo">
|
||||
<div class="kanban-col-header">
|
||||
@@ -1094,12 +1124,19 @@ async function confirmDelete() {
|
||||
|
||||
<!-- Done column -->
|
||||
<div class="kanban-col col-done">
|
||||
<div class="kanban-col-header">
|
||||
<button
|
||||
type="button"
|
||||
class="kanban-col-header col-toggle"
|
||||
:aria-expanded="expandedDone.has(groupKey(group))"
|
||||
@click="toggleDone(group)"
|
||||
>
|
||||
<span class="col-status-dot dot-done"></span>
|
||||
<span class="col-label">Done</span>
|
||||
<span class="col-count">{{ group.tasks.filter(t => t.status === 'done').length }}</span>
|
||||
</div>
|
||||
<div class="kanban-cards">
|
||||
<ChevronDown v-if="expandedDone.has(groupKey(group))" :size="14" />
|
||||
<ChevronRight v-else :size="14" />
|
||||
</button>
|
||||
<div v-if="expandedDone.has(groupKey(group))" class="kanban-cards">
|
||||
<router-link
|
||||
v-for="task in group.tasks.filter(t => t.status === 'done')"
|
||||
:key="task.id" :to="`/tasks/${task.id}`"
|
||||
@@ -1679,6 +1716,18 @@ async function confirmDelete() {
|
||||
}
|
||||
.col-status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||
.col-label { flex: 1; }
|
||||
/* The Done column's header is a button that folds its cards. It keeps the
|
||||
header's look — the reset is only what a <button> brings with it. */
|
||||
.col-toggle {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.col-toggle:focus-visible { outline: none; box-shadow: var(--fs-focus-ring); border-radius: var(--fs-radius-sm); }
|
||||
.col-count {
|
||||
background: var(--fs-surface-raised);
|
||||
border: 1px solid var(--fs-border-color);
|
||||
|
||||
Reference in New Issue
Block a user