From d0b0b342756cf6370cef734836deb2b20c2b3b8d Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 9 Jun 2026 20:45:07 +0200 Subject: [PATCH 1/2] Replace the global 'Show done' kanban toggle with per-column collapse so each user picks which columns to hide and the choice persists per profile The old board had a single 'Show done' toggle in the toolbar that hid every state marked done=true. That was a global on/off and could not distinguish DONE from CANCELLED, nor let a user hide e.g. WAIT without losing DONE too. Each column now has its own chevron in the header. A collapsed column shrinks to a 40px strip showing the rotated label, count chip, and an expand chevron; re-expanding restores the full column. State is stored per-profile under the localStorage key board_collapsed_columns (JSON array of state names), so switching profiles gives each its own layout and the choice survives reloads. The 'show done' toolbar toggle, its translation key, and the showDone arg threaded into useTasks are removed. useTasks now always fetches all states; per-column collapse is purely a render-time hide, so re-expanding a column is instant with no extra network round-trip. Drag and drop into a collapsed column is intentionally blocked: the compact strip has no drop zone, so a card cannot vanish into an invisible target. Drag-to-reorder of the column itself still works -- the rotated label is the drag handle. --- CHANGELOG.md | 10 +++ .../src/components/kanban/KanbanBoard.tsx | 37 +++----- .../src/components/kanban/KanbanColumn.tsx | 90 ++++++++++++++++++- frontend/src/hooks/useCollapsedColumns.ts | 89 ++++++++++++++++++ frontend/src/locales/de/kanban.json | 3 +- frontend/src/locales/en/kanban.json | 3 +- frontend/src/locales/es/kanban.json | 3 +- frontend/src/locales/ru/kanban.json | 3 +- 8 files changed, 208 insertions(+), 30 deletions(-) create mode 100644 frontend/src/hooks/useCollapsedColumns.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ebd3313..bd35d475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +- Replace the global "Show done" toggle on the kanban + board with per-column collapse. Each column has its own + chevron; collapsed columns shrink to a narrow strip with + a rotated label and task count, the choice persists per + profile, and re-expanding restores the full column. + Drops are blocked on collapsed columns so cards can't + vanish into an invisible drop zone. + ## 2.5.1 This release fixes a cluster of cloud-sync edge cases that diff --git a/frontend/src/components/kanban/KanbanBoard.tsx b/frontend/src/components/kanban/KanbanBoard.tsx index 0bda6dec..5447474d 100644 --- a/frontend/src/components/kanban/KanbanBoard.tsx +++ b/frontend/src/components/kanban/KanbanBoard.tsx @@ -35,18 +35,14 @@ import { reorderTasks as apiReorderTasks } from "../../api/client"; import { useReorderStates, useSettings } from "../../hooks/useSettings"; import type { ArchivedTask, Task } from "../../types"; import { ConfirmPopover } from "../common/ConfirmPopover"; -import { Toggle } from "../common/Toggle"; import { Button } from "../common/Button"; import { HelpButton } from "../common/HelpButton"; import { OpenInEditorButton } from "../common/OpenInEditorButton"; -import { - profileGet, - profileSet, -} from "../../utils/profileStorage"; import { SearchInput } from "../common/SearchInput"; import { PanelToolbar } from "../common/PanelToolbar"; import { ResizeHandle } from "../common/ResizeHandle"; import { useResizableColumns } from "../../hooks/useResizableColumns"; +import { useCollapsedColumns } from "../../hooks/useCollapsedColumns"; import { DOCS } from "../../docs/panelDocs"; import { TaskCard } from "./TaskCard"; import { KanbanColumn } from "./KanbanColumn"; @@ -347,13 +343,15 @@ export function KanbanBoard() { const { t } = useTranslation("kanban"); const { t: tc } = useTranslation("common"); const { t: tNav } = useTranslation("nav"); - const [showDone, setShowDone] = useState( - () => profileGet("board_show_done") === "true" - ); const [openAddInFirst, setOpenAddInFirst] = useState(false); const [search, setSearch] = useState(""); const searchRef = useRef(null); - const { data: rawTasks = [], isLoading } = useTasks(showDone); + // Always fetch all states; per-column collapse hides them + // visually without re-fetching, so re-expanding is instant. + const { data: rawTasks = [], isLoading } = useTasks(true); + const { + isCollapsed, toggle: toggleCollapsed, + } = useCollapsedColumns(); const tasks = rawTasks.filter((t) => matchesSearch(t, search)); const { pendingSearch, clearPendingSearch } = usePendingSearch(); @@ -406,8 +404,7 @@ export function KanbanBoard() { }) ); - const states = - settings?.task_states.filter((s) => showDone || !s.done) ?? []; + const states = settings?.task_states ?? []; const colCount = states.length; // Responsive column width: snap to 2, 3, or all columns @@ -632,20 +629,6 @@ export function KanbanBoard() { } right={<> - +
+ + {tasks.length} + +
+ {state.label || state.name} +
+
+ ); + } + return (
+
{/* Drop zone */} diff --git a/frontend/src/hooks/useCollapsedColumns.ts b/frontend/src/hooks/useCollapsedColumns.ts new file mode 100644 index 00000000..97cb99f3 --- /dev/null +++ b/frontend/src/hooks/useCollapsedColumns.ts @@ -0,0 +1,89 @@ +/** + * Per-profile persistence for collapsed kanban columns. + * + * Replaces the old single ``board_show_done`` flag. Now + * every column can be collapsed independently and the + * choice persists across reloads. + * + * Storage shape: a JSON array of task-state names (e.g. + * ``["DONE", "CANCELLED"]``) under + * ``board_collapsed_columns``. Profile-scoped via + * ``profileGet`` / ``profileSet`` so switching profiles + * gives each its own layout. + * + * A storage event-style custom event lets multiple + * components reading the same state stay in sync without + * a context provider — overkill for one consumer today, + * but the board renders many columns and tomorrow the + * dashboard may want the same. + */ +import { useCallback, useEffect, useState } from "react"; + +import { + profileGet, + profileSet, +} from "../utils/profileStorage"; + +const STORAGE_KEY = "board_collapsed_columns"; +const CHANGE_EVENT = "collapsed-columns-change"; + +function load(): Set { + const raw = profileGet(STORAGE_KEY); + if (!raw) return new Set(); + try { + const parsed = JSON.parse(raw) as unknown; + if (Array.isArray(parsed)) { + return new Set( + parsed.filter( + (item): item is string => typeof item === "string", + ), + ); + } + } catch { + // Corrupted value — fall through to a fresh set so + // the board never crashes on malformed local data. + } + return new Set(); +} + +function persist(state: Set): void { + profileSet( + STORAGE_KEY, JSON.stringify(Array.from(state)), + ); + window.dispatchEvent(new CustomEvent(CHANGE_EVENT)); +} + +export function useCollapsedColumns(): { + isCollapsed: (name: string) => boolean; + toggle: (name: string) => void; +} { + const [state, setState] = useState>(load); + + useEffect(() => { + function onChange() { + setState(load()); + } + window.addEventListener(CHANGE_EVENT, onChange); + return () => { + window.removeEventListener(CHANGE_EVENT, onChange); + }; + }, []); + + const isCollapsed = useCallback( + (name: string) => state.has(name), + [state], + ); + + const toggle = useCallback((name: string) => { + const next = new Set(state); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + persist(next); + setState(next); + }, [state]); + + return { isCollapsed, toggle }; +} diff --git a/frontend/src/locales/de/kanban.json b/frontend/src/locales/de/kanban.json index d8d7e9e5..b15061a4 100644 --- a/frontend/src/locales/de/kanban.json +++ b/frontend/src/locales/de/kanban.json @@ -2,7 +2,8 @@ "board": "Board", "loadingTasks": "Aufgaben laden...", "newTask": "Neue Aufgabe (Doppeltipp B)", - "showDone": "Erledigte anzeigen", + "collapseColumn": "Spalte einklappen", + "expandColumn": "Spalte ausklappen", "empty": "Leer", "addTask": "Aufgabe hinzuf\u00fcgen", "taskTitle": "Aufgabentitel", diff --git a/frontend/src/locales/en/kanban.json b/frontend/src/locales/en/kanban.json index 919e6ee9..8262f53f 100644 --- a/frontend/src/locales/en/kanban.json +++ b/frontend/src/locales/en/kanban.json @@ -2,7 +2,8 @@ "board": "Board", "loadingTasks": "Loading tasks...", "newTask": "New task (double-tap B)", - "showDone": "Show done", + "collapseColumn": "Collapse column", + "expandColumn": "Expand column", "empty": "Empty", "addTask": "Add task", "taskTitle": "Task title", diff --git a/frontend/src/locales/es/kanban.json b/frontend/src/locales/es/kanban.json index 72d9257e..7870bb62 100644 --- a/frontend/src/locales/es/kanban.json +++ b/frontend/src/locales/es/kanban.json @@ -2,7 +2,8 @@ "board": "Tablero", "loadingTasks": "Cargando tareas...", "newTask": "Nueva tarea (doble clic en B)", - "showDone": "Mostrar completadas", + "collapseColumn": "Colapsar columna", + "expandColumn": "Expandir columna", "empty": "Vac\u00edo", "addTask": "A\u00f1adir tarea", "taskTitle": "T\u00edtulo de la tarea", diff --git a/frontend/src/locales/ru/kanban.json b/frontend/src/locales/ru/kanban.json index 4e95ab8c..8e196b0e 100644 --- a/frontend/src/locales/ru/kanban.json +++ b/frontend/src/locales/ru/kanban.json @@ -2,7 +2,8 @@ "board": "Доска", "loadingTasks": "Загрузка задач...", "newTask": "Новая задача (двойное нажатие B)", - "showDone": "Показать выполненные", + "collapseColumn": "Свернуть колонку", + "expandColumn": "Развернуть колонку", "empty": "Пусто", "addTask": "Добавить задачу", "taskTitle": "Название задачи", From fedf4f4a98438a3663c6c677c1128fb95f4865b3 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 9 Jun 2026 20:54:14 +0200 Subject: [PATCH 2/2] Align the collapsed-column chevron with the chevrons on open columns by mirroring the open column's header/body split The first iteration wrapped the whole collapsed column in a single dashed-border box with py-2 padding, which pushed the expand chevron ~8px below the collapse chevrons on the open columns next to it. The eye picks that misalignment up immediately when collapsed and open columns sit side by side. Split the collapsed column into the same two-section layout the open column uses: a top header row (flex items-center mb-3 px-1) holding the expand chevron, and a bordered body below (rounded-lg border border-dashed border-border-subtle bg-surface-card/30) holding the dot, count chip, and rotated label. The header row geometry matches the open column's exactly, so the chevron lines up regardless of how many of each kind sit beside each other. The body inherits the open column's drop-zone styling, which also makes the visual transition between expanded and collapsed states look like one shape changing rather than two unrelated boxes. --- .../src/components/kanban/KanbanColumn.tsx | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/kanban/KanbanColumn.tsx b/frontend/src/components/kanban/KanbanColumn.tsx index 3159513e..39c0e104 100644 --- a/frontend/src/components/kanban/KanbanColumn.tsx +++ b/frontend/src/components/kanban/KanbanColumn.tsx @@ -104,11 +104,15 @@ export function KanbanColumn({ } if (collapsed) { - // Compact strip: rotated label + count + expand chevron. - // Drag handle (column reorder) and the expand toggle - // are the only interactive surfaces; the drop zone is - // intentionally absent so cards cannot be dropped into - // an invisible column. + // Compact strip. The header row mirrors the open + // column's ``flex items-center gap-2 mb-3 px-1`` so + // the expand chevron lines up vertically with the + // collapse chevrons on the open columns next to it. + // The body below mirrors the open column's drop zone + // — dashed border, rounded corners, same background — + // but holds the dot + count chip + rotated label + // instead of cards. No drop target on purpose: cards + // must not vanish into an invisible column. return (
- + {/* Header: same Y as the open column's header */} +
+ +
+ {/* Body: matches the open column's drop-zone */}
- - {tasks.length} - -
- {state.label || state.name} +
+ + {tasks.length} + +
+ {state.label || state.name} +
);