From 14c6e87a651630f62984fd839f0140f2ab90aebd Mon Sep 17 00:00:00 2001 From: Ku6epXBOCTuK Date: Sat, 18 Jul 2026 08:32:34 +0500 Subject: [PATCH] feat: separate column toggle buttons --- src/kanban/buildKanbanBoard.test.ts | 45 ++++++++++++++--- src/kanban/buildKanbanBoard.ts | 10 ++-- src/kanban/messages.test.ts | 30 ++++++++--- src/kanban/messages.ts | 18 +++---- src/kanban/outboundMessages.ts | 4 +- src/views/taskKanbanHtml.ts | 77 ++++++++++++++++++++++++----- src/views/taskKanbanPanel.ts | 27 +++++++--- 7 files changed, 166 insertions(+), 45 deletions(-) diff --git a/src/kanban/buildKanbanBoard.test.ts b/src/kanban/buildKanbanBoard.test.ts index ad0e609..2e42022 100644 --- a/src/kanban/buildKanbanBoard.test.ts +++ b/src/kanban/buildKanbanBoard.test.ts @@ -21,7 +21,11 @@ function task(partial: Partial & Pick): Task { }; } -const visibleStatuses = TASK_STATUS_ORDER.filter( +const defaultHiddenStatuses = TASK_STATUS_ORDER.filter( + (status) => TASK_STATUS_META[status].defaultHidden, +); + +const visibleByDefaultStatuses = TASK_STATUS_ORDER.filter( (status) => !TASK_STATUS_META[status].defaultHidden, ); @@ -65,19 +69,21 @@ describe("buildKanbanBoard", () => { }), ]; - it("hides defaultHidden statuses by default", () => { + it("hides all defaultHidden statuses by default", () => { const board = buildKanbanBoard(tasks); const statuses = board.columns.map((col) => col.status); - expect(statuses).toEqual(visibleStatuses); + expect(statuses).toEqual(visibleByDefaultStatuses); for (const col of board.columns) { expect(TASK_STATUS_META[col.status].defaultHidden).toBe(false); expect(col.label).toBe(TASK_STATUS_META[col.status].label); } }); - it("includes all statuses when showHiddenStatuses is true", () => { - const board = buildKanbanBoard(tasks, { showHiddenStatuses: true }); + it("includes all statuses when all defaultHidden statuses are in shownHiddenColumns", () => { + const board = buildKanbanBoard(tasks, { + shownHiddenColumns: [...defaultHiddenStatuses], + }); expect(board.columns.map((col) => col.status)).toEqual(TASK_STATUS_ORDER); for (const col of board.columns) { @@ -85,8 +91,31 @@ describe("buildKanbanBoard", () => { } }); + it("shows only selected hidden columns", () => { + const board = buildKanbanBoard(tasks, { + shownHiddenColumns: [TaskStatus.BACKLOG], + }); + const statuses = board.columns.map((col) => col.status); + + expect(statuses).toContain(TaskStatus.BACKLOG); + expect(statuses).not.toContain(TaskStatus.CANCELLED); + expect(statuses).not.toContain(TaskStatus.UNKNOWN); + for (const status of visibleByDefaultStatuses) { + expect(statuses).toContain(status); + } + }); + + it("empty shownHiddenColumns hides all defaultHidden columns", () => { + const board = buildKanbanBoard(tasks, { shownHiddenColumns: [] }); + const statuses = board.columns.map((col) => col.status); + + expect(statuses).toEqual(visibleByDefaultStatuses); + }); + it("places each task into the column matching its status", () => { - const board = buildKanbanBoard(tasks, { showHiddenStatuses: true }); + const board = buildKanbanBoard(tasks, { + shownHiddenColumns: [...defaultHiddenStatuses], + }); for (const col of board.columns) { for (const t of col.tasks) { @@ -113,7 +142,9 @@ describe("buildKanbanBoard", () => { it("returns empty visible columns when there are no tasks", () => { const board = buildKanbanBoard([]); - expect(board.columns.map((col) => col.status)).toEqual(visibleStatuses); + expect(board.columns.map((col) => col.status)).toEqual( + visibleByDefaultStatuses, + ); for (const col of board.columns) { expect(col.tasks).toEqual([]); } diff --git a/src/kanban/buildKanbanBoard.ts b/src/kanban/buildKanbanBoard.ts index 1981669..96f8b65 100644 --- a/src/kanban/buildKanbanBoard.ts +++ b/src/kanban/buildKanbanBoard.ts @@ -16,8 +16,8 @@ export type KanbanBoard = { }; export type BuildKanbanBoardOptions = { - /** When false (default), columns with meta.defaultHidden are omitted. */ - showHiddenStatuses?: boolean; + /** `defaultHidden`-statuses to show; empty/undefined = hide all hidden. */ + shownHiddenColumns?: TaskStatus[]; }; /** Build status columns; empty columns kept. Hidden statuses optional. */ @@ -25,9 +25,11 @@ export function buildKanbanBoard( tasks: Task[], options: BuildKanbanBoardOptions = {}, ): KanbanBoard { - const showHidden = options.showHiddenStatuses ?? false; + const extraVisible = options.shownHiddenColumns ?? []; const statuses = TASK_STATUS_ORDER.filter( - (status) => showHidden || !TASK_STATUS_META[status].defaultHidden, + (status) => + !TASK_STATUS_META[status].defaultHidden || + extraVisible.includes(status), ); const columns: KanbanColumn[] = statuses.map((status) => { diff --git a/src/kanban/messages.test.ts b/src/kanban/messages.test.ts index 0bda6e4..f18e19c 100644 --- a/src/kanban/messages.test.ts +++ b/src/kanban/messages.test.ts @@ -49,12 +49,6 @@ describe("parseKanbanInboundMessage", () => { ).toEqual(ok({ variant: "setScopeFilter", scope: "all" })); }); - it("parses setShowHidden", () => { - expect( - parseKanbanInboundMessage({ variant: "setShowHidden", showHidden: true }), - ).toEqual(ok({ variant: "setShowHidden", showHidden: true })); - }); - it("parses createTask", () => { expect( parseKanbanInboundMessage({ @@ -64,6 +58,30 @@ describe("parseKanbanInboundMessage", () => { ).toEqual(ok({ variant: "createTask", status: TaskStatus.BACKLOG })); }); + it("parses toggleHiddenColumn", () => { + expect( + parseKanbanInboundMessage({ + variant: "toggleHiddenColumn", + status: TaskStatus.BACKLOG, + }), + ).toEqual( + ok({ + variant: "toggleHiddenColumn", + status: TaskStatus.BACKLOG, + } satisfies KanbanInboundMessage), + ); + }); + + it("rejects toggleHiddenColumn with invalid status", () => { + const result = parseKanbanInboundMessage({ + variant: "toggleHiddenColumn", + status: "__not_a_status__", + }); + expect(result.isErr()).toBe(true); + if (result.isOk()) return; + expect(result.error.variant).toBe(AppErrorVariant.INVALID_MESSAGE); + }); + it("rejects non-objects", () => { const result = parseKanbanInboundMessage(null); expect(result.isErr()).toBe(true); diff --git a/src/kanban/messages.ts b/src/kanban/messages.ts index 0b656af..cc79a3f 100644 --- a/src/kanban/messages.ts +++ b/src/kanban/messages.ts @@ -11,8 +11,8 @@ export type KanbanInboundMessage = | { variant: "refresh" } | { variant: "moveTask"; id: string; status: TaskStatus } | { variant: "setScopeFilter"; scope: KanbanScopeFilter } - | { variant: "setShowHidden"; showHidden: boolean } - | { variant: "createTask"; status: TaskStatus }; + | { variant: "createTask"; status: TaskStatus } + | { variant: "toggleHiddenColumn"; status: TaskStatus }; export function isKanbanScopeFilter( value: unknown, @@ -66,13 +66,6 @@ export function parseKanbanInboundMessage( return ok({ variant: "setScopeFilter", scope: raw.scope }); } - case "setShowHidden": { - if (typeof raw.showHidden !== "boolean") { - return invalid(variant); - } - return ok({ variant: "setShowHidden", showHidden: raw.showHidden }); - } - case "createTask": { if (!isTaskStatus(raw.status)) { return invalid(variant); @@ -80,6 +73,13 @@ export function parseKanbanInboundMessage( return ok({ variant: "createTask", status: raw.status }); } + case "toggleHiddenColumn": { + if (!isTaskStatus(raw.status)) { + return invalid(variant); + } + return ok({ variant: "toggleHiddenColumn", status: raw.status }); + } + default: return invalid(variant); } diff --git a/src/kanban/outboundMessages.ts b/src/kanban/outboundMessages.ts index 80dd6d3..9aef87c 100644 --- a/src/kanban/outboundMessages.ts +++ b/src/kanban/outboundMessages.ts @@ -1,5 +1,6 @@ import { AppError } from "@/error"; import { TaskScope } from "@/model/taskLocation"; +import { TaskStatus } from "@/model/taskStatus"; import { KanbanBoard } from "./buildKanbanBoard"; import { KanbanScopeFilter } from "./messages"; @@ -9,7 +10,8 @@ export type KanbanOutboundMessage = variant: "state"; board: KanbanBoard; scopeFilter: KanbanScopeFilter; - showHidden: boolean; + shownHiddenColumns: TaskStatus[]; + defaultHiddenStatuses: TaskStatus[]; availableScopes: TaskScope[]; } | { variant: "error"; error: AppError }; diff --git a/src/views/taskKanbanHtml.ts b/src/views/taskKanbanHtml.ts index 9324fd0..92493d1 100644 --- a/src/views/taskKanbanHtml.ts +++ b/src/views/taskKanbanHtml.ts @@ -76,6 +76,46 @@ export function getTaskKanbanHtml( line-height: 1.2; min-width: 24px; } + button.eye-btn { + padding: 0 4px; + font-size: 0.85em; + line-height: 1.2; + min-width: 20px; + background: transparent; + border: 1px solid transparent; + color: var(--vscode-foreground); + opacity: 0.6; + cursor: pointer; + } + button.eye-btn:hover { opacity: 1; } + #visibility-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-widget-border)); + font-size: 0.85em; + } + #visibility-bar .label { + opacity: 0.7; + margin-right: 4px; + } + #visibility-bar button { + font: inherit; + cursor: pointer; + padding: 2px 8px; + border: 1px solid var(--vscode-input-border, transparent); + border-radius: 2px; + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-size: 0.85em; + } + #visibility-bar button.shown { + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-color: transparent; + } #status-line { font-size: 0.85em; opacity: 0.85; @@ -147,11 +187,11 @@ export function getTaskKanbanHtml(

Kanban

- +