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={<> - + + {/* Body: matches the open column's drop-zone */} +
+
+ + {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": "Название задачи",