Files
xboct-flow/src/views/taskDashboardPanel.ts
T

228 lines
5.7 KiB
TypeScript

import * as vscode from "vscode";
import { listLocatedTasks } from "@/commands/listLocatedTasks";
import { resolveTaskLocations } from "@/commands/resolveTaskLocations";
import { saveTaskDescription } from "@/commands/saveTaskDescription";
import { buildDashboardView } from "@/dashboard/buildDashboardView";
import { TaskFilter } from "@/dashboard/filterTasks";
import { GroupBy } from "@/dashboard/groupBy";
import {
parseDashboardInboundMessage,
type DashboardInboundMessage,
} from "@/dashboard/messages";
import { DashboardOutboundMessage } from "@/dashboard/outboundMessages";
import { appError, AppErrorVariant } from "@/error";
import { LocatedTask } from "@/model/taskLocation";
import { IConfigProvider, ITaskRepository } from "@/ports";
import { presentError } from "@/ui/presentError";
import { createNonce, getTaskDashboardHtml } from "./taskDashboardHtml";
export type TaskDashboardDeps = {
repo: ITaskRepository;
config: IConfigProvider;
/** Called after dashboard mutates tasks (e.g. save description). */
onTasksMutated?: () => void;
};
export type TaskDashboardShowOptions = {
/** Pre-select this task after load (e.g. open from tree). */
selectedId?: string;
};
/**
* Host adapter for the Task Dashboard webview.
* Owns panel lifecycle and message wiring; business rules stay in use-cases.
*/
export class TaskDashboardPanel {
static #current: TaskDashboardPanel | undefined;
static show(
deps: TaskDashboardDeps,
options: TaskDashboardShowOptions = {},
): void {
if (TaskDashboardPanel.#current) {
const current = TaskDashboardPanel.#current;
if (options.selectedId !== undefined) {
current.#selectedId = options.selectedId;
}
current.#panel.reveal(vscode.ViewColumn.One);
void current.reloadTasks();
return;
}
const panel = vscode.window.createWebviewPanel(
"xboctFlow.dashboard",
"XBOCT flow",
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
},
);
TaskDashboardPanel.#current = new TaskDashboardPanel(
panel,
deps,
options.selectedId,
);
}
static refreshIfOpen(): void {
void TaskDashboardPanel.#current?.reloadTasks();
}
readonly #panel: vscode.WebviewPanel;
readonly #deps: TaskDashboardDeps;
#tasks: LocatedTask[] = [];
#filter: TaskFilter = {};
#groupBy: GroupBy = GroupBy.NONE;
#selectedId: string | undefined;
#disposed = false;
constructor(
panel: vscode.WebviewPanel,
deps: TaskDashboardDeps,
selectedId?: string,
) {
this.#panel = panel;
this.#deps = deps;
this.#selectedId = selectedId;
const nonce = createNonce();
this.#panel.webview.html = getTaskDashboardHtml(this.#panel.webview, nonce);
this.#panel.webview.onDidReceiveMessage((raw: unknown) => {
void this.#onMessage(raw);
});
this.#panel.onDidDispose(() => {
this.#disposed = true;
if (TaskDashboardPanel.#current === this) {
TaskDashboardPanel.#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;
if (
this.#selectedId &&
!this.#tasks.some((item) => item.task.id === this.#selectedId)
) {
this.#selectedId = undefined;
}
this.#postState();
}
async #onMessage(raw: unknown): Promise<void> {
const parsed = parseDashboardInboundMessage(raw);
if (parsed.isErr()) {
this.#post({ type: "error", error: parsed.error });
return;
}
await this.#handleInbound(parsed.value);
}
async #handleInbound(message: DashboardInboundMessage): Promise<void> {
switch (message.type) {
case "ready":
case "refresh":
await this.reloadTasks();
return;
case "selectTask":
this.#selectedId = message.id;
this.#postState();
return;
case "setFilter":
this.#filter = message.filter;
this.#postState();
return;
case "setGroupBy":
this.#groupBy = message.groupBy;
this.#postState();
return;
case "saveDescription":
await this.#saveDescription(message.id, message.description);
return;
case "createTask":
await vscode.commands.executeCommand("xboctFlow.createTask");
await this.reloadTasks();
return;
}
}
async #saveDescription(id: string, description: string): 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;
}
const result = await saveTaskDescription(this.#deps.repo, {
folderPath: located.location.folderPath,
id,
description,
});
if (result.isErr()) {
this.#post({ type: "error", error: result.error });
return;
}
const saved = result.value;
this.#tasks = this.#tasks.map((item) =>
item.task.id === saved.id
? { task: saved, location: item.location }
: item,
);
this.#selectedId = saved.id;
this.#post({ type: "saved", task: saved });
this.#postState();
this.#deps.onTasksMutated?.();
}
#postState(): void {
const plainTasks = this.#tasks.map((item) => item.task);
const view = buildDashboardView(plainTasks, {
filter: this.#filter,
groupBy: this.#groupBy,
selectedId: this.#selectedId,
});
this.#post({
type: "state",
filter: this.#filter,
groupBy: this.#groupBy,
selectedId: this.#selectedId,
groups: view.groups,
selected: view.selected ?? null,
});
}
#post(message: DashboardOutboundMessage): void {
if (this.#disposed) return;
void this.#panel.webview.postMessage(message);
}
}