feat: add kanban board
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import * as vscode from "vscode";
|
||||
import { changeStatus } from "@/commands/changeStatus";
|
||||
import { listLocatedTasks } from "@/commands/listLocatedTasks";
|
||||
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
|
||||
import { buildKanbanBoard } from "@/kanban/buildKanbanBoard";
|
||||
import {
|
||||
parseKanbanInboundMessage,
|
||||
type KanbanInboundMessage,
|
||||
} from "@/kanban/messages";
|
||||
import { KanbanOutboundMessage } from "@/kanban/outboundMessages";
|
||||
import { appError, AppErrorVariant } from "@/error";
|
||||
import { LocatedTask } from "@/model/taskLocation";
|
||||
import { TaskStatus } from "@/model/task";
|
||||
import { IConfigProvider, ITaskRepository } from "@/ports";
|
||||
import { presentError } from "@/ui/presentError";
|
||||
import { createKanbanNonce, getTaskKanbanHtml } from "./taskKanbanHtml";
|
||||
|
||||
export type TaskKanbanDeps = {
|
||||
repo: ITaskRepository;
|
||||
config: IConfigProvider;
|
||||
onTasksMutated?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Host adapter for the Kanban webview.
|
||||
* Load → buildKanbanBoard; move → changeStatus via location cache.
|
||||
*/
|
||||
export class TaskKanbanPanel {
|
||||
static #current: TaskKanbanPanel | undefined;
|
||||
|
||||
static show(deps: TaskKanbanDeps): void {
|
||||
if (TaskKanbanPanel.#current) {
|
||||
TaskKanbanPanel.#current.#panel.reveal(vscode.ViewColumn.One);
|
||||
void TaskKanbanPanel.#current.reloadTasks();
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"xboctFlow.kanban",
|
||||
"XBOCT flow — Kanban",
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
);
|
||||
|
||||
TaskKanbanPanel.#current = new TaskKanbanPanel(panel, deps);
|
||||
}
|
||||
|
||||
static refreshIfOpen(): void {
|
||||
void TaskKanbanPanel.#current?.reloadTasks();
|
||||
}
|
||||
|
||||
readonly #panel: vscode.WebviewPanel;
|
||||
readonly #deps: TaskKanbanDeps;
|
||||
|
||||
#tasks: LocatedTask[] = [];
|
||||
#disposed = false;
|
||||
|
||||
constructor(panel: vscode.WebviewPanel, deps: TaskKanbanDeps) {
|
||||
this.#panel = panel;
|
||||
this.#deps = deps;
|
||||
|
||||
const nonce = createKanbanNonce();
|
||||
this.#panel.webview.html = getTaskKanbanHtml(this.#panel.webview, nonce);
|
||||
|
||||
this.#panel.webview.onDidReceiveMessage((raw: unknown) => {
|
||||
void this.#onMessage(raw);
|
||||
});
|
||||
|
||||
this.#panel.onDidDispose(() => {
|
||||
this.#disposed = true;
|
||||
if (TaskKanbanPanel.#current === this) {
|
||||
TaskKanbanPanel.#current = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async reloadTasks(): Promise<void> {
|
||||
if (this.#disposed) return;
|
||||
|
||||
const locations = resolveTaskLocations(this.#deps.config);
|
||||
const result = await listLocatedTasks(this.#deps.repo, locations);
|
||||
|
||||
if (result.isErr()) {
|
||||
this.#post({ type: "error", error: result.error });
|
||||
if (result.error.variant === AppErrorVariant.NO_FOLDER) {
|
||||
presentError(result.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.#tasks = result.value;
|
||||
this.#postState();
|
||||
}
|
||||
|
||||
async #onMessage(raw: unknown): Promise<void> {
|
||||
const parsed = parseKanbanInboundMessage(raw);
|
||||
if (parsed.isErr()) {
|
||||
this.#post({ type: "error", error: parsed.error });
|
||||
return;
|
||||
}
|
||||
await this.#handleInbound(parsed.value);
|
||||
}
|
||||
|
||||
async #handleInbound(message: KanbanInboundMessage): Promise<void> {
|
||||
switch (message.type) {
|
||||
case "ready":
|
||||
case "refresh":
|
||||
await this.reloadTasks();
|
||||
return;
|
||||
|
||||
case "moveTask":
|
||||
await this.#moveTask(message.id, message.status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async #moveTask(id: string, status: TaskStatus): Promise<void> {
|
||||
const located = this.#tasks.find((item) => item.task.id === id);
|
||||
if (!located) {
|
||||
this.#post({
|
||||
type: "error",
|
||||
error: appError(AppErrorVariant.NOT_FOUND, { id }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (located.task.status === status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await changeStatus(this.#deps.repo, {
|
||||
folderPath: located.location.folderPath,
|
||||
id,
|
||||
status,
|
||||
});
|
||||
|
||||
if (result.isErr()) {
|
||||
this.#post({ type: "error", error: result.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = result.value;
|
||||
this.#tasks = this.#tasks.map((item) =>
|
||||
item.task.id === updated.id
|
||||
? { task: updated, location: item.location }
|
||||
: item,
|
||||
);
|
||||
this.#postState();
|
||||
this.#deps.onTasksMutated?.();
|
||||
}
|
||||
|
||||
#postState(): void {
|
||||
const board = buildKanbanBoard(this.#tasks.map((item) => item.task));
|
||||
this.#post({ type: "state", board });
|
||||
}
|
||||
|
||||
#post(message: KanbanOutboundMessage): void {
|
||||
if (this.#disposed) return;
|
||||
void this.#panel.webview.postMessage(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user