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;
|
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) {
|
function toggleMilestoneCollapse(id: number) {
|
||||||
if (collapsedMilestones.value.has(id)) {
|
if (collapsedMilestones.value.has(id)) {
|
||||||
collapsedMilestones.value.delete(id);
|
collapsedMilestones.value.delete(id);
|
||||||
@@ -386,6 +405,16 @@ async function loadTasks() {
|
|||||||
all.push(...next.notes);
|
all.push(...next.notes);
|
||||||
}
|
}
|
||||||
tasks.value = all;
|
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 {
|
} catch {
|
||||||
// Say so. This used to swallow the error and leave an empty board, which is
|
// 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
|
// 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 v-for="group in milestoneGroups" :key="group.milestone?.id ?? 'unassigned'" class="milestone-group">
|
||||||
<div
|
<div
|
||||||
class="milestone-header"
|
class="milestone-header"
|
||||||
:class="{ clickable: !!group.milestone && renamingMilestoneId !== group.milestone?.id }"
|
:class="{ clickable: renamingMilestoneId !== group.milestone?.id }"
|
||||||
@click="group.milestone && renamingMilestoneId !== group.milestone.id && toggleMilestoneCollapse(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">
|
<span class="ms-chevron">
|
||||||
<ChevronRight v-if="collapsedMilestones.has(group.milestone.id)" :size="16" />
|
<ChevronRight v-if="collapsedMilestones.has(groupKey(group))" :size="16" />
|
||||||
<ChevronDown v-else :size="16" />
|
<ChevronDown v-else :size="16" />
|
||||||
</span>
|
</span>
|
||||||
<template v-if="group.milestone && renamingMilestoneId === group.milestone.id">
|
<template v-if="group.milestone && renamingMilestoneId === group.milestone.id">
|
||||||
@@ -1023,7 +1053,7 @@ async function confirmDelete() {
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!group.milestone || !collapsedMilestones.has(group.milestone.id)" class="kanban">
|
<div v-if="!collapsedMilestones.has(groupKey(group))" class="kanban">
|
||||||
<!-- Todo column -->
|
<!-- Todo column -->
|
||||||
<div class="kanban-col col-todo">
|
<div class="kanban-col col-todo">
|
||||||
<div class="kanban-col-header">
|
<div class="kanban-col-header">
|
||||||
@@ -1094,12 +1124,19 @@ async function confirmDelete() {
|
|||||||
|
|
||||||
<!-- Done column -->
|
<!-- Done column -->
|
||||||
<div class="kanban-col col-done">
|
<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-status-dot dot-done"></span>
|
||||||
<span class="col-label">Done</span>
|
<span class="col-label">Done</span>
|
||||||
<span class="col-count">{{ group.tasks.filter(t => t.status === 'done').length }}</span>
|
<span class="col-count">{{ group.tasks.filter(t => t.status === 'done').length }}</span>
|
||||||
</div>
|
<ChevronDown v-if="expandedDone.has(groupKey(group))" :size="14" />
|
||||||
<div class="kanban-cards">
|
<ChevronRight v-else :size="14" />
|
||||||
|
</button>
|
||||||
|
<div v-if="expandedDone.has(groupKey(group))" class="kanban-cards">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="task in group.tasks.filter(t => t.status === 'done')"
|
v-for="task in group.tasks.filter(t => t.status === 'done')"
|
||||||
:key="task.id" :to="`/tasks/${task.id}`"
|
: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-status-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.col-label { flex: 1; }
|
.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 {
|
.col-count {
|
||||||
background: var(--fs-surface-raised);
|
background: var(--fs-surface-raised);
|
||||||
border: 1px solid var(--fs-border-color);
|
border: 1px solid var(--fs-border-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user