feat: add buttons - open task in dashboard, open dashboard
This commit is contained in:
+39
-3
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user