feat: add buttons - open task in dashboard, open dashboard

This commit is contained in:
2026-07-15 00:59:27 +05:00
parent beeb9dd7f9
commit 0454a32d8c
8 changed files with 130 additions and 26 deletions
+39 -3
View File
@@ -46,7 +46,13 @@
},
{
"command": "xboctFlow.openDashboard",
"title": "XBOCT flow: Open Dashboard"
"title": "XBOCT flow: Open Dashboard",
"icon": "$(dashboard)"
},
{
"command": "xboctFlow.openInDashboard",
"title": "XBOCT flow: Edit in Dashboard",
"icon": "$(edit)"
}
],
"configuration": {
@@ -98,7 +104,12 @@
{
"command": "xboctFlow.createTask",
"when": "view == xboctFlow.tasks",
"group": "navigation"
"group": "navigation@1"
},
{
"command": "xboctFlow.openDashboard",
"when": "view == xboctFlow.tasks",
"group": "navigation@2"
}
],
"view/item/context": [
@@ -107,15 +118,40 @@
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "inline@1"
},
{
"command": "xboctFlow.openInDashboard",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "inline@2"
},
{
"command": "xboctFlow.changeStatus",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@1"
"group": "inline@3"
},
{
"command": "xboctFlow.deleteTask",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "inline@4"
},
{
"command": "xboctFlow.openTask",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@1"
},
{
"command": "xboctFlow.openInDashboard",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@2"
},
{
"command": "xboctFlow.changeStatus",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@3"
},
{
"command": "xboctFlow.deleteTask",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@4"
}
]
}
+6
View File
@@ -60,6 +60,12 @@ describe("parseDashboardInboundMessage", () => {
);
});
it("parses createTask", () => {
expect(parseDashboardInboundMessage({ type: "createTask" })).toEqual(
ok({ type: "createTask" } satisfies DashboardInboundMessage),
);
});
it("rejects non-objects", () => {
const result = parseDashboardInboundMessage(null);
expect(result.isErr()).toBe(true);
+3 -1
View File
@@ -9,7 +9,8 @@ export type DashboardInboundMessage =
| { type: "setFilter"; filter: TaskFilter }
| { type: "setGroupBy"; groupBy: GroupBy }
| { type: "saveDescription"; id: string; description: string }
| { type: "refresh" };
| { type: "refresh" }
| { type: "createTask" };
const GROUP_BY_VALUES: ReadonlySet<string> = new Set([
"none",
@@ -44,6 +45,7 @@ export function parseDashboardInboundMessage(
switch (type) {
case "ready":
case "refresh":
case "createTask":
return ok({ type });
case "selectTask": {
-10
View File
@@ -50,16 +50,6 @@ export function activate(context: vscode.ExtensionContext) {
tree,
onTasksMutated: () => TaskDashboardPanel.refreshIfOpen(),
});
context.subscriptions.push(
vscode.commands.registerCommand("xboctFlow.openDashboard", () => {
TaskDashboardPanel.show({
repo,
config,
onTasksMutated: () => tree.refresh(),
});
}),
);
}
export function deactivate() {}
+35
View File
@@ -8,6 +8,7 @@ import { appError, AppErrorVariant } from "@/error";
import { TaskLocation } from "@/model/taskLocation";
import { TaskStatus } from "@/model/task";
import { IConfigProvider, ITaskRepository } from "@/ports";
import { TaskDashboardPanel } from "@/views/taskDashboardPanel";
import { TaskTreeProvider } from "@/views/taskTreeProvider";
import { presentError } from "./presentError";
import { resolveTaskRef } from "./resolveTaskRef";
@@ -21,6 +22,14 @@ export type TaskCommandDeps = {
onTasksMutated?: () => void;
};
function dashboardDeps(deps: TaskCommandDeps) {
return {
repo: deps.repo,
config: deps.config,
onTasksMutated: () => deps.tree.refresh(),
};
}
function notifyMutated(deps: TaskCommandDeps): void {
deps.tree.refresh();
deps.onTasksMutated?.();
@@ -177,6 +186,25 @@ export async function runChangeStatus(
);
}
export async function runOpenInDashboard(
deps: TaskCommandDeps,
item?: unknown,
): Promise<void> {
const ref = await resolveTaskRef(deps.repo, deps.config, item);
if (ref === undefined) {
if (resolveTaskLocations(deps.config).length === 0) {
presentError(appError(AppErrorVariant.NO_FOLDER));
}
return;
}
TaskDashboardPanel.show(dashboardDeps(deps), { selectedId: ref.id });
}
export function runOpenDashboard(deps: TaskCommandDeps): void {
TaskDashboardPanel.show(dashboardDeps(deps));
}
/** Register all task command adapters on the extension host. */
export function registerTaskCommands(
context: vscode.ExtensionContext,
@@ -198,5 +226,12 @@ export function registerTaskCommands(
"xboctFlow.changeStatus",
(item?: unknown) => runChangeStatus(deps, item),
),
vscode.commands.registerCommand("xboctFlow.openDashboard", () =>
runOpenDashboard(deps),
),
vscode.commands.registerCommand(
"xboctFlow.openInDashboard",
(item?: unknown) => runOpenInDashboard(deps, item),
),
);
}
+7 -1
View File
@@ -199,6 +199,7 @@ export function getTaskDashboardHtml(
<option value="status">Status</option>
<option value="priority">Priority</option>
</select>
<button id="createTask" type="button" title="Create task">+ Task</button>
<button id="resetFilters" type="button" class="secondary" title="Reset filters">Reset</button>
<button id="refresh" type="button" class="secondary" title="Refresh">↻</button>
</div>
@@ -208,7 +209,7 @@ export function getTaskDashboardHtml(
<div id="list"></div>
</aside>
<main>
<div id="empty">Select a task or create one from the sidebar.</div>
<div id="empty">Select a task or create one with «+ Task».</div>
<div id="detail">
<h1 id="detail-title"></h1>
<div id="detail-meta"></div>
@@ -251,6 +252,7 @@ export function getTaskDashboardHtml(
query: document.getElementById("query"),
groupBy: document.getElementById("groupBy"),
refresh: document.getElementById("refresh"),
createTask: document.getElementById("createTask"),
resetFilters: document.getElementById("resetFilters"),
statusFilters: document.getElementById("status-filters"),
priorityFilters: document.getElementById("priority-filters"),
@@ -437,6 +439,10 @@ export function getTaskDashboardHtml(
post({ type: "refresh" });
});
el.createTask.addEventListener("click", () => {
post({ type: "createTask" });
});
el.resetFilters.addEventListener("click", () => {
resetFilters();
});
+32 -6
View File
@@ -23,6 +23,11 @@ export type TaskDashboardDeps = {
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.
@@ -30,16 +35,23 @@ export type TaskDashboardDeps = {
export class TaskDashboardPanel {
static #current: TaskDashboardPanel | undefined;
static show(deps: TaskDashboardDeps): void {
static show(
deps: TaskDashboardDeps,
options: TaskDashboardShowOptions = {},
): void {
if (TaskDashboardPanel.#current) {
TaskDashboardPanel.#current.#panel.reveal(vscode.ViewColumn.One);
void TaskDashboardPanel.#current.reloadTasks();
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",
"Task Dashboard",
"XBOCT flow",
vscode.ViewColumn.One,
{
enableScripts: true,
@@ -47,7 +59,11 @@ export class TaskDashboardPanel {
},
);
TaskDashboardPanel.#current = new TaskDashboardPanel(panel, deps);
TaskDashboardPanel.#current = new TaskDashboardPanel(
panel,
deps,
options.selectedId,
);
}
static refreshIfOpen(): void {
@@ -63,9 +79,14 @@ export class TaskDashboardPanel {
#selectedId: string | undefined;
#disposed = false;
constructor(panel: vscode.WebviewPanel, deps: TaskDashboardDeps) {
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);
@@ -140,6 +161,11 @@ export class TaskDashboardPanel {
case "saveDescription":
await this.#saveDescription(message.id, message.description);
return;
case "createTask":
await vscode.commands.executeCommand("xboctFlow.createTask");
await this.reloadTasks();
return;
}
}
+8 -5
View File
@@ -45,11 +45,14 @@ headless/тестов можно оставить; editor-save — optional path
ломать format-on-save историю: raw-save тоже лучше через editor path, если
нужен паритет с VS Code.
[ ] **Боковая панель** В боковой панели должны быть
[x] **Боковая панель**
- кнопка открытия дешборда
- кнопки для - удаления, редактирования через дешборд
- кнопка открытия dashboard (view title → `xboctFlow.openDashboard`)
- кнопки на задаче (inline): open file, edit in dashboard
(`xboctFlow.openInDashboard` + preselect), delete
- change status остаётся в context menu
[ ] **Dashboard**
[x] **Dashboard: кнопка создания задачи**
- кнопка создания задачи
- `+ Task` в toolbar → inbound `createTask` → host
`executeCommand("xboctFlow.createTask")` → reload list