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", "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": { "configuration": {
@@ -98,7 +104,12 @@
{ {
"command": "xboctFlow.createTask", "command": "xboctFlow.createTask",
"when": "view == xboctFlow.tasks", "when": "view == xboctFlow.tasks",
"group": "navigation" "group": "navigation@1"
},
{
"command": "xboctFlow.openDashboard",
"when": "view == xboctFlow.tasks",
"group": "navigation@2"
} }
], ],
"view/item/context": [ "view/item/context": [
@@ -107,15 +118,40 @@
"when": "view == xboctFlow.tasks && viewItem == task", "when": "view == xboctFlow.tasks && viewItem == task",
"group": "inline@1" "group": "inline@1"
}, },
{
"command": "xboctFlow.openInDashboard",
"when": "view == xboctFlow.tasks && viewItem == task",
"group": "inline@2"
},
{ {
"command": "xboctFlow.changeStatus", "command": "xboctFlow.changeStatus",
"when": "view == xboctFlow.tasks && viewItem == task", "when": "view == xboctFlow.tasks && viewItem == task",
"group": "1_task@1" "group": "inline@3"
}, },
{ {
"command": "xboctFlow.deleteTask", "command": "xboctFlow.deleteTask",
"when": "view == xboctFlow.tasks && viewItem == task", "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" "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", () => { it("rejects non-objects", () => {
const result = parseDashboardInboundMessage(null); const result = parseDashboardInboundMessage(null);
expect(result.isErr()).toBe(true); expect(result.isErr()).toBe(true);
+3 -1
View File
@@ -9,7 +9,8 @@ export type DashboardInboundMessage =
| { type: "setFilter"; filter: TaskFilter } | { type: "setFilter"; filter: TaskFilter }
| { type: "setGroupBy"; groupBy: GroupBy } | { type: "setGroupBy"; groupBy: GroupBy }
| { type: "saveDescription"; id: string; description: string } | { type: "saveDescription"; id: string; description: string }
| { type: "refresh" }; | { type: "refresh" }
| { type: "createTask" };
const GROUP_BY_VALUES: ReadonlySet<string> = new Set([ const GROUP_BY_VALUES: ReadonlySet<string> = new Set([
"none", "none",
@@ -44,6 +45,7 @@ export function parseDashboardInboundMessage(
switch (type) { switch (type) {
case "ready": case "ready":
case "refresh": case "refresh":
case "createTask":
return ok({ type }); return ok({ type });
case "selectTask": { case "selectTask": {
-10
View File
@@ -50,16 +50,6 @@ export function activate(context: vscode.ExtensionContext) {
tree, tree,
onTasksMutated: () => TaskDashboardPanel.refreshIfOpen(), onTasksMutated: () => TaskDashboardPanel.refreshIfOpen(),
}); });
context.subscriptions.push(
vscode.commands.registerCommand("xboctFlow.openDashboard", () => {
TaskDashboardPanel.show({
repo,
config,
onTasksMutated: () => tree.refresh(),
});
}),
);
} }
export function deactivate() {} export function deactivate() {}
+35
View File
@@ -8,6 +8,7 @@ import { appError, AppErrorVariant } from "@/error";
import { TaskLocation } from "@/model/taskLocation"; import { TaskLocation } from "@/model/taskLocation";
import { TaskStatus } from "@/model/task"; import { TaskStatus } from "@/model/task";
import { IConfigProvider, ITaskRepository } from "@/ports"; import { IConfigProvider, ITaskRepository } from "@/ports";
import { TaskDashboardPanel } from "@/views/taskDashboardPanel";
import { TaskTreeProvider } from "@/views/taskTreeProvider"; import { TaskTreeProvider } from "@/views/taskTreeProvider";
import { presentError } from "./presentError"; import { presentError } from "./presentError";
import { resolveTaskRef } from "./resolveTaskRef"; import { resolveTaskRef } from "./resolveTaskRef";
@@ -21,6 +22,14 @@ export type TaskCommandDeps = {
onTasksMutated?: () => void; onTasksMutated?: () => void;
}; };
function dashboardDeps(deps: TaskCommandDeps) {
return {
repo: deps.repo,
config: deps.config,
onTasksMutated: () => deps.tree.refresh(),
};
}
function notifyMutated(deps: TaskCommandDeps): void { function notifyMutated(deps: TaskCommandDeps): void {
deps.tree.refresh(); deps.tree.refresh();
deps.onTasksMutated?.(); 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. */ /** Register all task command adapters on the extension host. */
export function registerTaskCommands( export function registerTaskCommands(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
@@ -198,5 +226,12 @@ export function registerTaskCommands(
"xboctFlow.changeStatus", "xboctFlow.changeStatus",
(item?: unknown) => runChangeStatus(deps, item), (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="status">Status</option>
<option value="priority">Priority</option> <option value="priority">Priority</option>
</select> </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="resetFilters" type="button" class="secondary" title="Reset filters">Reset</button>
<button id="refresh" type="button" class="secondary" title="Refresh">↻</button> <button id="refresh" type="button" class="secondary" title="Refresh">↻</button>
</div> </div>
@@ -208,7 +209,7 @@ export function getTaskDashboardHtml(
<div id="list"></div> <div id="list"></div>
</aside> </aside>
<main> <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"> <div id="detail">
<h1 id="detail-title"></h1> <h1 id="detail-title"></h1>
<div id="detail-meta"></div> <div id="detail-meta"></div>
@@ -251,6 +252,7 @@ export function getTaskDashboardHtml(
query: document.getElementById("query"), query: document.getElementById("query"),
groupBy: document.getElementById("groupBy"), groupBy: document.getElementById("groupBy"),
refresh: document.getElementById("refresh"), refresh: document.getElementById("refresh"),
createTask: document.getElementById("createTask"),
resetFilters: document.getElementById("resetFilters"), resetFilters: document.getElementById("resetFilters"),
statusFilters: document.getElementById("status-filters"), statusFilters: document.getElementById("status-filters"),
priorityFilters: document.getElementById("priority-filters"), priorityFilters: document.getElementById("priority-filters"),
@@ -437,6 +439,10 @@ export function getTaskDashboardHtml(
post({ type: "refresh" }); post({ type: "refresh" });
}); });
el.createTask.addEventListener("click", () => {
post({ type: "createTask" });
});
el.resetFilters.addEventListener("click", () => { el.resetFilters.addEventListener("click", () => {
resetFilters(); resetFilters();
}); });
+32 -6
View File
@@ -23,6 +23,11 @@ export type TaskDashboardDeps = {
onTasksMutated?: () => void; 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. * Host adapter for the Task Dashboard webview.
* Owns panel lifecycle and message wiring; business rules stay in use-cases. * Owns panel lifecycle and message wiring; business rules stay in use-cases.
@@ -30,16 +35,23 @@ export type TaskDashboardDeps = {
export class TaskDashboardPanel { export class TaskDashboardPanel {
static #current: TaskDashboardPanel | undefined; static #current: TaskDashboardPanel | undefined;
static show(deps: TaskDashboardDeps): void { static show(
deps: TaskDashboardDeps,
options: TaskDashboardShowOptions = {},
): void {
if (TaskDashboardPanel.#current) { if (TaskDashboardPanel.#current) {
TaskDashboardPanel.#current.#panel.reveal(vscode.ViewColumn.One); const current = TaskDashboardPanel.#current;
void TaskDashboardPanel.#current.reloadTasks(); if (options.selectedId !== undefined) {
current.#selectedId = options.selectedId;
}
current.#panel.reveal(vscode.ViewColumn.One);
void current.reloadTasks();
return; return;
} }
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"xboctFlow.dashboard", "xboctFlow.dashboard",
"Task Dashboard", "XBOCT flow",
vscode.ViewColumn.One, vscode.ViewColumn.One,
{ {
enableScripts: true, 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 { static refreshIfOpen(): void {
@@ -63,9 +79,14 @@ export class TaskDashboardPanel {
#selectedId: string | undefined; #selectedId: string | undefined;
#disposed = false; #disposed = false;
constructor(panel: vscode.WebviewPanel, deps: TaskDashboardDeps) { constructor(
panel: vscode.WebviewPanel,
deps: TaskDashboardDeps,
selectedId?: string,
) {
this.#panel = panel; this.#panel = panel;
this.#deps = deps; this.#deps = deps;
this.#selectedId = selectedId;
const nonce = createNonce(); const nonce = createNonce();
this.#panel.webview.html = getTaskDashboardHtml(this.#panel.webview, nonce); this.#panel.webview.html = getTaskDashboardHtml(this.#panel.webview, nonce);
@@ -140,6 +161,11 @@ export class TaskDashboardPanel {
case "saveDescription": case "saveDescription":
await this.#saveDescription(message.id, message.description); await this.#saveDescription(message.id, message.description);
return; 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, если ломать format-on-save историю: raw-save тоже лучше через editor path, если
нужен паритет с VS Code. нужен паритет с 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