feat: add dashboard ui view
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import * as vscode from "vscode";
|
||||
import { loadDashboard } from "@/commands/loadDashboard";
|
||||
import { saveTaskDescription } from "@/commands/saveTaskDescription";
|
||||
import { buildDashboardView } from "@/dashboard/buildDashboardView";
|
||||
import { TaskFilter } from "@/dashboard/filterTasks";
|
||||
import { GroupBy } from "@/dashboard/groupTasks";
|
||||
import {
|
||||
parseDashboardInboundMessage,
|
||||
type DashboardInboundMessage,
|
||||
} from "@/dashboard/messages";
|
||||
import { DashboardOutboundMessage } from "@/dashboard/outboundMessages";
|
||||
import { AppErrorVariant } from "@/error";
|
||||
import { Task } from "@/model/task";
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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): void {
|
||||
if (TaskDashboardPanel.#current) {
|
||||
TaskDashboardPanel.#current.#panel.reveal(vscode.ViewColumn.One);
|
||||
void TaskDashboardPanel.#current.reloadTasks();
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
"projectTasks.dashboard",
|
||||
"Task Dashboard",
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
);
|
||||
|
||||
TaskDashboardPanel.#current = new TaskDashboardPanel(panel, deps);
|
||||
}
|
||||
|
||||
static refreshIfOpen(): void {
|
||||
void TaskDashboardPanel.#current?.reloadTasks();
|
||||
}
|
||||
|
||||
readonly #panel: vscode.WebviewPanel;
|
||||
readonly #deps: TaskDashboardDeps;
|
||||
|
||||
#tasks: Task[] = [];
|
||||
#filter: TaskFilter = {};
|
||||
#groupBy: GroupBy = "none";
|
||||
#selectedId: string | undefined;
|
||||
#disposed = false;
|
||||
|
||||
constructor(panel: vscode.WebviewPanel, deps: TaskDashboardDeps) {
|
||||
this.#panel = panel;
|
||||
this.#deps = deps;
|
||||
|
||||
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 folderPath = this.#deps.config.getProjectTaskPath();
|
||||
const result = await loadDashboard(this.#deps.repo, { folderPath });
|
||||
|
||||
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.tasks;
|
||||
if (
|
||||
this.#selectedId &&
|
||||
!this.#tasks.some((task) => 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;
|
||||
}
|
||||
}
|
||||
|
||||
async #saveDescription(id: string, description: string): Promise<void> {
|
||||
const folderPath = this.#deps.config.getProjectTaskPath();
|
||||
const result = await saveTaskDescription(this.#deps.repo, {
|
||||
folderPath,
|
||||
id,
|
||||
description,
|
||||
});
|
||||
|
||||
if (result.isErr()) {
|
||||
this.#post({ type: "error", error: result.error });
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = result.value;
|
||||
this.#tasks = this.#tasks.map((task) =>
|
||||
task.id === saved.id ? saved : task,
|
||||
);
|
||||
this.#selectedId = saved.id;
|
||||
this.#post({ type: "saved", task: saved });
|
||||
this.#postState();
|
||||
this.#deps.onTasksMutated?.();
|
||||
}
|
||||
|
||||
#postState(): void {
|
||||
const view = buildDashboardView(this.#tasks, {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user