feat: update kanban view, dashboard view, add backlog status

This commit is contained in:
2026-07-15 03:14:03 +05:00
parent 84f64af62d
commit e46437a512
16 changed files with 451 additions and 141 deletions
+114 -9
View File
@@ -1,18 +1,21 @@
import * as vscode from "vscode";
import { changeStatus } from "@/commands/changeStatus";
import { createTask } from "@/commands/createTask";
import { listLocatedTasks } from "@/commands/listLocatedTasks";
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
import { buildKanbanBoard } from "@/kanban/buildKanbanBoard";
import {
parseKanbanInboundMessage,
type KanbanInboundMessage,
type KanbanScopeFilter,
} from "@/kanban/messages";
import { KanbanOutboundMessage } from "@/kanban/outboundMessages";
import { appError, AppErrorVariant } from "@/error";
import { LocatedTask } from "@/model/taskLocation";
import { TaskStatus } from "@/model/task";
import { LocatedTask, TaskLocation, TaskScope } from "@/model/taskLocation";
import { TaskStatus } from "@/model/taskStatus";
import { IConfigProvider, ITaskRepository } from "@/ports";
import { presentError } from "@/ui/presentError";
import { TASK_SCOPE_LABELS } from "@/ui/taskLabels";
import { createKanbanNonce, getTaskKanbanHtml } from "./taskKanbanHtml";
export type TaskKanbanDeps = {
@@ -56,6 +59,8 @@ export class TaskKanbanPanel {
readonly #deps: TaskKanbanDeps;
#tasks: LocatedTask[] = [];
#scopeFilter: KanbanScopeFilter = "all";
#showHidden = false;
#disposed = false;
constructor(panel: vscode.WebviewPanel, deps: TaskKanbanDeps) {
@@ -84,7 +89,7 @@ export class TaskKanbanPanel {
const result = await listLocatedTasks(this.#deps.repo, locations);
if (result.isErr()) {
this.#post({ type: "error", error: result.error });
this.#post({ variant: "error", error: result.error });
if (result.error.variant === AppErrorVariant.NO_FOLDER) {
presentError(result.error);
}
@@ -98,14 +103,14 @@ export class TaskKanbanPanel {
async #onMessage(raw: unknown): Promise<void> {
const parsed = parseKanbanInboundMessage(raw);
if (parsed.isErr()) {
this.#post({ type: "error", error: parsed.error });
this.#post({ variant: "error", error: parsed.error });
return;
}
await this.#handleInbound(parsed.value);
}
async #handleInbound(message: KanbanInboundMessage): Promise<void> {
switch (message.type) {
switch (message.variant) {
case "ready":
case "refresh":
await this.reloadTasks();
@@ -114,6 +119,20 @@ export class TaskKanbanPanel {
case "moveTask":
await this.#moveTask(message.id, message.status);
return;
case "setScopeFilter":
this.#scopeFilter = message.scope;
this.#postState();
return;
case "setShowHidden":
this.#showHidden = message.showHidden;
this.#postState();
return;
case "createTask":
await this.#createInColumn(message.status);
return;
}
}
@@ -121,7 +140,7 @@ export class TaskKanbanPanel {
const located = this.#tasks.find((item) => item.task.id === id);
if (!located) {
this.#post({
type: "error",
variant: "error",
error: appError(AppErrorVariant.NOT_FOUND, { id }),
});
return;
@@ -138,7 +157,7 @@ export class TaskKanbanPanel {
});
if (result.isErr()) {
this.#post({ type: "error", error: result.error });
this.#post({ variant: "error", error: result.error });
return;
}
@@ -152,9 +171,95 @@ export class TaskKanbanPanel {
this.#deps.onTasksMutated?.();
}
async #createInColumn(status: TaskStatus): Promise<void> {
const location = await this.#pickLocationForCreate();
if (location === undefined) {
return;
}
const title = await vscode.window.showInputBox({
prompt: "Task title",
placeHolder: "Enter task title...",
});
if (title === undefined) {
return;
}
const result = await createTask(this.#deps.repo, {
folderPath: location.folderPath,
title,
status,
});
if (result.isErr()) {
presentError(result.error);
this.#post({ variant: "error", error: result.error });
return;
}
await this.reloadTasks();
this.#deps.onTasksMutated?.();
}
async #pickLocationForCreate(): Promise<TaskLocation | undefined> {
const locations = resolveTaskLocations(this.#deps.config);
if (locations.length === 0) {
presentError(appError(AppErrorVariant.NO_FOLDER));
return undefined;
}
if (this.#scopeFilter !== "all") {
const match = locations.find((loc) => loc.scope === this.#scopeFilter);
if (match) {
return match;
}
}
if (locations.length === 1) {
return locations[0];
}
const picked = await vscode.window.showQuickPick(
locations.map((location) => ({
label: TASK_SCOPE_LABELS[location.scope],
description: location.folderPath,
location,
})),
{ placeHolder: "Create task in…" },
);
return picked?.location;
}
#filteredTasks(): LocatedTask[] {
if (this.#scopeFilter === "all") {
return this.#tasks;
}
return this.#tasks.filter(
(item) => item.location.scope === this.#scopeFilter,
);
}
#availableScopes(): TaskScope[] {
const scopes = new Set(this.#tasks.map((item) => item.location.scope));
// Also expose configured locations even if empty (for filter + create).
for (const loc of resolveTaskLocations(this.#deps.config)) {
scopes.add(loc.scope);
}
return [...scopes];
}
#postState(): void {
const board = buildKanbanBoard(this.#tasks.map((item) => item.task));
this.#post({ type: "state", board });
const board = buildKanbanBoard(
this.#filteredTasks().map((item) => item.task),
{ showHiddenStatuses: this.#showHidden },
);
this.#post({
variant: "state",
board,
scopeFilter: this.#scopeFilter,
showHidden: this.#showHidden,
availableScopes: this.#availableScopes(),
});
}
#post(message: KanbanOutboundMessage): void {