From cb8647ba5ae39b092b521f9221b4e404d2bf476f Mon Sep 17 00:00:00 2001 From: Gordon Date: Sat, 17 Apr 2021 01:28:17 +0100 Subject: [PATCH] Allow opening multiple burndown panels --- ext-src/KanbnBoardPanel.ts | 196 ++++++++++++++---------------- ext-src/KanbnBurndownPanel.ts | 98 ++++++++------- ext-src/KanbnTaskPanel.ts | 221 ++++++++++++++++------------------ ext-src/extension.ts | 220 ++++++++++++++++----------------- ext-src/getNonce.ts | 8 ++ src/App.tsx | 11 +- src/Burndown.tsx | 100 +++++++++++++-- src/index.css | 66 +++++++++- 8 files changed, 515 insertions(+), 405 deletions(-) create mode 100644 ext-src/getNonce.ts diff --git a/ext-src/KanbnBoardPanel.ts b/ext-src/KanbnBoardPanel.ts index 9dbe12b..2692979 100644 --- a/ext-src/KanbnBoardPanel.ts +++ b/ext-src/KanbnBoardPanel.ts @@ -1,23 +1,24 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import KanbnTaskPanel from './KanbnTaskPanel'; -import KanbnBurndownPanel from './KanbnBurndownPanel'; +import * as path from "path"; +import * as vscode from "vscode"; +import getNonce from "./getNonce"; +import KanbnTaskPanel from "./KanbnTaskPanel"; +import KanbnBurndownPanel from "./KanbnBurndownPanel"; export default class KanbnBoardPanel { public static currentPanel: KanbnBoardPanel | undefined; - private static readonly viewType = 'react'; + private static readonly viewType = "react"; private readonly _panel: vscode.WebviewPanel; private readonly _extensionPath: string; private readonly _workspacePath: string; - private readonly _kanbn: typeof import('@basementuniverse/kanbn/src/main'); + private readonly _kanbn: typeof import("@basementuniverse/kanbn/src/main"); private _disposables: vscode.Disposable[] = []; public static createOrShow( extensionPath: string, workspacePath: string, - kanbn: typeof import('@basementuniverse/kanbn/src/main') + kanbn: typeof import("@basementuniverse/kanbn/src/main") ) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; @@ -45,23 +46,23 @@ export default class KanbnBoardPanel { } let tasks: any[]; try { - tasks = (await KanbnBoardPanel.currentPanel._kanbn.loadAllTrackedTasks(index)).map( - task => KanbnBoardPanel.currentPanel!._kanbn.hydrateTask(index, task) + tasks = (await KanbnBoardPanel.currentPanel._kanbn.loadAllTrackedTasks(index)).map((task) => + KanbnBoardPanel.currentPanel!._kanbn.hydrateTask(index, task) ); } catch (error) { vscode.window.showErrorMessage(error instanceof Error ? error.message : error); return; } KanbnBoardPanel.currentPanel._panel.webview.postMessage({ - type: 'index', + type: "index", index, tasks, hiddenColumns: index.options.hiddenColumns ?? [], startedColumns: index.options.startedColumns ?? [], completedColumns: index.options.completedColumns ?? [], dateFormat: KanbnBoardPanel.currentPanel._kanbn.getDateFormat(index), - showBurndownButton: vscode.workspace.getConfiguration('kanbn').get('showBurndownButton'), - showSprintButton: vscode.workspace.getConfiguration('kanbn').get('showSprintButton') + showBurndownButton: vscode.workspace.getConfiguration("kanbn").get("showBurndownButton"), + showSprintButton: vscode.workspace.getConfiguration("kanbn").get("showSprintButton"), }); } } @@ -70,14 +71,14 @@ export default class KanbnBoardPanel { extensionPath: string, workspacePath: string, column: vscode.ViewColumn, - kanbn: typeof import('@basementuniverse/kanbn/src/main') + kanbn: typeof import("@basementuniverse/kanbn/src/main") ) { this._extensionPath = extensionPath; this._workspacePath = workspacePath; this._kanbn = kanbn; // Create and show a new webview panel - this._panel = vscode.window.createWebviewPanel(KanbnBoardPanel.viewType, 'Kanbn Board', column, { + this._panel = vscode.window.createWebviewPanel(KanbnBoardPanel.viewType, "Kanbn Board", column, { // Enable javascript in the webview enableScripts: true, @@ -86,18 +87,18 @@ export default class KanbnBoardPanel { // Restrict the webview to only loading content from allowed paths localResourceRoots: [ - vscode.Uri.file(path.join(this._extensionPath, 'build')), + vscode.Uri.file(path.join(this._extensionPath, "build")), vscode.Uri.file(path.join(this._workspacePath, this._kanbn.getFolderName())), - vscode.Uri.file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist')) - ] + vscode.Uri.file(path.join(this._extensionPath, "node_modules", "vscode-codicons", "dist")), + ], }); (this._panel as any).iconPath = { - light: vscode.Uri.file(path.join(this._extensionPath, 'resources', 'project_light.svg')), - dark: vscode.Uri.file(path.join(this._extensionPath, 'resources', 'project_dark.svg')) + light: vscode.Uri.file(path.join(this._extensionPath, "resources", "project_light.svg")), + dark: vscode.Uri.file(path.join(this._extensionPath, "resources", "project_dark.svg")), }; // Set the webview's title to the kanbn project name - this._kanbn.getIndex().then(index => { + this._kanbn.getIndex().then((index) => { this._panel.title = index.name; }); @@ -109,73 +110,65 @@ export default class KanbnBoardPanel { this._panel.onDidDispose(() => this.dispose(), null, this._disposables); // Handle messages from the webview - this._panel.webview.onDidReceiveMessage(async message => { - switch (message.command) { + this._panel.webview.onDidReceiveMessage( + async (message) => { + switch (message.command) { + // Display error message + case "error": + vscode.window.showErrorMessage(message.text); + return; - // Display error message - case 'error': - vscode.window.showErrorMessage(message.text); - return; + case "kanbn.task": + KanbnTaskPanel.show( + this._extensionPath, + this._workspacePath, + this._kanbn, + message.taskId, + message.columnName + ); + return; - case 'kanbn.task': - KanbnTaskPanel.show( - this._extensionPath, - this._workspacePath, - this._kanbn, - message.taskId, - message.columnName - ); - return; - - // Move a task - case 'kanbn.move': - try { - await kanbn.moveTask(message.task, message.columnName, message.position); - } catch (e) { - vscode.window.showErrorMessage(e.message); - } - return; - - // Create a task - case 'kanbn.addTask': - KanbnTaskPanel.show( - this._extensionPath, - this._workspacePath, - this._kanbn, - null, - message.columnName - ); - return; - - // Open a burndown chart - case 'kanbn.burndown': - KanbnBurndownPanel.createOrShow( - this._extensionPath, - this._workspacePath, - this._kanbn - ); - KanbnBurndownPanel.update(); - return; - - // Start a new sprint - case 'kanbn.sprint': - - // Prompt for a sprint name - const newSprintName = await vscode.window.showInputBox({ - placeHolder: 'The sprint name.' - }); - - // If the input prompt wasn't cancelled, start a new sprint - if (newSprintName !== undefined) { + // Move a task + case "kanbn.move": try { - await kanbn.sprint(newSprintName, '', new Date()); + await kanbn.moveTask(message.task, message.columnName, message.position); } catch (e) { vscode.window.showErrorMessage(e.message); } - } - return; - } - }, null, this._disposables); + return; + + // Create a task + case "kanbn.addTask": + KanbnTaskPanel.show(this._extensionPath, this._workspacePath, this._kanbn, null, message.columnName); + return; + + // Open a burndown chart + case "kanbn.burndown": + KanbnBurndownPanel.show(this._extensionPath, this._workspacePath, this._kanbn); + KanbnBurndownPanel.updateAll(); + return; + + // Start a new sprint + case "kanbn.sprint": + // Prompt for a sprint name + const newSprintName = await vscode.window.showInputBox({ + placeHolder: "The sprint name.", + }); + + // If the input prompt wasn't cancelled, start a new sprint + if (newSprintName !== undefined) { + try { + await kanbn.sprint(newSprintName, "", new Date()); + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + } + return; + } + }, + null, + this._disposables + ); } public dispose() { @@ -192,21 +185,21 @@ export default class KanbnBoardPanel { } private _getHtmlForWebview() { - const manifest = require(path.join(this._extensionPath, 'build', 'asset-manifest.json')); - const mainScript = manifest['main.js']; - const mainStyle = manifest['main.css']; - const scriptUri = vscode.Uri - .file(path.join(this._extensionPath, 'build', mainScript)) - .with({ scheme: 'vscode-resource' }); - const styleUri = vscode.Uri - .file(path.join(this._extensionPath, 'build', mainStyle)) - .with({ scheme: 'vscode-resource' }); - const customStyleUri = vscode.Uri - .file(path.join(this._workspacePath, this._kanbn.getFolderName(), 'board.css')) - .with({ scheme: 'vscode-resource' }); - const codiconsUri = vscode.Uri - .file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css')) - .with({ scheme: 'vscode-resource' }); + const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json")); + const mainScript = manifest["main.js"]; + const mainStyle = manifest["main.css"]; + const scriptUri = vscode.Uri.file(path.join(this._extensionPath, "build", mainScript)).with({ + scheme: "vscode-resource", + }); + const styleUri = vscode.Uri.file(path.join(this._extensionPath, "build", mainStyle)).with({ + scheme: "vscode-resource", + }); + const customStyleUri = vscode.Uri.file( + path.join(this._workspacePath, this._kanbn.getFolderName(), "board.css") + ).with({ scheme: "vscode-resource" }); + const codiconsUri = vscode.Uri.file( + path.join(this._extensionPath, "node_modules", "vscode-codicons", "dist", "codicon.css") + ).with({ scheme: "vscode-resource" }); // Use a nonce to whitelist which scripts can be run const nonce = getNonce(); @@ -222,7 +215,7 @@ export default class KanbnBoardPanel { - + @@ -232,12 +225,3 @@ export default class KanbnBoardPanel { `; } } - -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} diff --git a/ext-src/KanbnBurndownPanel.ts b/ext-src/KanbnBurndownPanel.ts index 72c883f..df7d34a 100644 --- a/ext-src/KanbnBurndownPanel.ts +++ b/ext-src/KanbnBurndownPanel.ts @@ -1,73 +1,72 @@ import * as path from "path"; import * as vscode from "vscode"; +import getNonce from "./getNonce"; +import { v4 as uuidv4 } from "uuid"; export default class KanbnBurndownPanel { - public static currentPanel: KanbnBurndownPanel | undefined; - private static readonly viewType = "react"; + private static panels: Record = {}; private readonly _panel: vscode.WebviewPanel; private readonly _extensionPath: string; private readonly _workspacePath: string; private readonly _kanbn: typeof import("@basementuniverse/kanbn/src/main"); + private readonly _panelUuid: string; private _disposables: vscode.Disposable[] = []; - public static createOrShow( + public static async show( extensionPath: string, workspacePath: string, kanbn: typeof import("@basementuniverse/kanbn/src/main") ) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; - // If we already have a panel, show it, otherwise create a new panel - if (KanbnBurndownPanel.currentPanel) { - KanbnBurndownPanel.currentPanel._panel.reveal(column); - } else { - KanbnBurndownPanel.currentPanel = new KanbnBurndownPanel( - extensionPath, - workspacePath, - column || vscode.ViewColumn.One, - kanbn - ); - } + // Create a panel + const panelUuid = uuidv4(); + const burndownPanel = new KanbnBurndownPanel( + extensionPath, + workspacePath, + column || vscode.ViewColumn.One, + kanbn, + panelUuid + ); + KanbnBurndownPanel.panels[panelUuid] = burndownPanel; } - public static async update() { - if (KanbnBurndownPanel.currentPanel) { - let index: any; - try { - index = await KanbnBurndownPanel.currentPanel._kanbn.getIndex(); - } catch (error) { - vscode.window.showErrorMessage(error instanceof Error ? error.message : error); - return; - } - let tasks: any[]; - try { - tasks = (await KanbnBurndownPanel.currentPanel._kanbn.loadAllTrackedTasks(index)).map( - task => KanbnBurndownPanel.currentPanel!._kanbn.hydrateTask(index, task) - ); - } catch (error) { - vscode.window.showErrorMessage(error instanceof Error ? error.message : error); - return; - } - KanbnBurndownPanel.currentPanel._panel.webview.postMessage({ - type: "burndown", - index, - tasks, - dateFormat: KanbnBurndownPanel.currentPanel._kanbn.getDateFormat(index), - }); + public static async updateAll() { + const panels = Object.values(KanbnBurndownPanel.panels); + if (panels.length === 0) { + return; } + const kanbn = panels[0]._kanbn; + let index: any; + try { + index = await kanbn.getIndex(); + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + return; + } + let tasks: any[]; + try { + tasks = (await kanbn.loadAllTrackedTasks(index)).map((task) => kanbn.hydrateTask(index, task)); + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + return; + } + panels.forEach((panel) => panel._update(index, tasks)); } private constructor( extensionPath: string, workspacePath: string, column: vscode.ViewColumn, - kanbn: typeof import("@basementuniverse/kanbn/src/main") + kanbn: typeof import("@basementuniverse/kanbn/src/main"), + panelUuid: string ) { this._extensionPath = extensionPath; this._workspacePath = workspacePath; this._kanbn = kanbn; + this._panelUuid = panelUuid; // Create and show a new webview panel this._panel = vscode.window.createWebviewPanel(KanbnBurndownPanel.viewType, "Burndown Chart", column, { @@ -117,9 +116,6 @@ export default class KanbnBurndownPanel { } public dispose() { - KanbnBurndownPanel.currentPanel = undefined; - - // Clean up our resources this._panel.dispose(); while (this._disposables.length) { const x = this._disposables.pop(); @@ -129,6 +125,15 @@ export default class KanbnBurndownPanel { } } + private async _update(index: any, tasks: any[]) { + this._panel.webview.postMessage({ + type: "burndown", + index, + tasks, + dateFormat: this._kanbn.getDateFormat(index), + }); + } + private _getHtmlForWebview() { const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json")); const mainScript = manifest["main.js"]; @@ -170,12 +175,3 @@ export default class KanbnBurndownPanel { `; } } - -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} diff --git a/ext-src/KanbnTaskPanel.ts b/ext-src/KanbnTaskPanel.ts index 582b985..2c4bd23 100644 --- a/ext-src/KanbnTaskPanel.ts +++ b/ext-src/KanbnTaskPanel.ts @@ -1,6 +1,7 @@ -import * as path from 'path'; -import * as vscode from 'vscode'; -import { v4 as uuidv4 } from 'uuid'; +import * as path from "path"; +import * as vscode from "vscode"; +import getNonce from "./getNonce"; +import { v4 as uuidv4 } from "uuid"; function transformTaskData(taskData: any) { const result = { @@ -8,71 +9,69 @@ function transformTaskData(taskData: any) { name: taskData.name, description: taskData.description, metadata: { - created: taskData.metadata.created - ? new Date(taskData.metadata.created) - : new Date(), + created: taskData.metadata.created ? new Date(taskData.metadata.created) : new Date(), updated: new Date(), assigned: taskData.metadata.assigned, progress: taskData.progress, - tags: taskData.metadata.tags + tags: taskData.metadata.tags, } as any, relations: taskData.relations, subTasks: taskData.subTasks, comments: taskData.comments.map((comment: any) => ({ author: comment.author, date: new Date(Date.parse(comment.date)), - text: comment.text - })) + text: comment.text, + })), } as any; // Add assigned if (taskData.metadata.assigned) { - result.metadata['assigned'] = taskData.metadata.assigned; + result.metadata["assigned"] = taskData.metadata.assigned; } // Add progress if (taskData.progress > 0) { - result.metadata['progress'] = taskData.progress; + result.metadata["progress"] = taskData.progress; } // Add tags if (taskData.metadata.tags.length) { - result.metadata['tags'] = taskData.metadata.tags; + result.metadata["tags"] = taskData.metadata.tags; } // Add due, started and completed dates if present if (taskData.metadata.due) { - result.metadata['due'] = new Date(Date.parse(taskData.metadata.due)); + result.metadata["due"] = new Date(Date.parse(taskData.metadata.due)); } if (taskData.metadata.started) { - result.metadata['started'] = new Date(Date.parse(taskData.metadata.started)); + result.metadata["started"] = new Date(Date.parse(taskData.metadata.started)); } if (taskData.metadata.completed) { - result.metadata['completed'] = new Date(Date.parse(taskData.metadata.completed)); + result.metadata["completed"] = new Date(Date.parse(taskData.metadata.completed)); } return result; } export default class KanbnTaskPanel { - private static readonly viewType = 'react'; + private static readonly viewType = "react"; private static panels: Record = {}; private readonly _panel: vscode.WebviewPanel; private readonly _extensionPath: string; private readonly _workspacePath: string; - private readonly _kanbn: typeof import('@basementuniverse/kanbn/src/main'); - private _taskId: string|null; - private _columnName: string|null; + private readonly _kanbn: typeof import("@basementuniverse/kanbn/src/main"); private readonly _panelUuid: string; + private _taskId: string | null; + private _columnName: string | null; private _disposables: vscode.Disposable[] = []; public static async show( extensionPath: string, workspacePath: string, - kanbn: typeof import('@basementuniverse/kanbn/src/main'), - taskId: string|null, - columnName: string|null + kanbn: typeof import("@basementuniverse/kanbn/src/main"), + taskId: string | null, + columnName: string | null ) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; @@ -95,9 +94,9 @@ export default class KanbnTaskPanel { extensionPath: string, workspacePath: string, column: vscode.ViewColumn, - kanbn: typeof import('@basementuniverse/kanbn/src/main'), - taskId: string|null, - columnName: string|null, + kanbn: typeof import("@basementuniverse/kanbn/src/main"), + taskId: string | null, + columnName: string | null, panelUuid: string ) { this._extensionPath = extensionPath; @@ -108,7 +107,7 @@ export default class KanbnTaskPanel { this._panelUuid = panelUuid; // Create and show a new webview panel - this._panel = vscode.window.createWebviewPanel(KanbnTaskPanel.viewType, 'New task', column, { + this._panel = vscode.window.createWebviewPanel(KanbnTaskPanel.viewType, "New task", column, { // Enable javascript in the webview enableScripts: true, @@ -117,19 +116,19 @@ export default class KanbnTaskPanel { // Restrict the webview to only loading content from allowed paths localResourceRoots: [ - vscode.Uri.file(path.join(this._extensionPath, 'build')), + vscode.Uri.file(path.join(this._extensionPath, "build")), vscode.Uri.file(path.join(this._workspacePath, this._kanbn.getFolderName())), - vscode.Uri.file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist')) - ] + vscode.Uri.file(path.join(this._extensionPath, "node_modules", "vscode-codicons", "dist")), + ], }); (this._panel as any).iconPath = { - light: vscode.Uri.file(path.join(this._extensionPath, 'resources', 'task_light.svg')), - dark: vscode.Uri.file(path.join(this._extensionPath, 'resources', 'task_dark.svg')) + light: vscode.Uri.file(path.join(this._extensionPath, "resources", "task_light.svg")), + dark: vscode.Uri.file(path.join(this._extensionPath, "resources", "task_dark.svg")), }; // Set the webview's title to the kanbn task name if (this._taskId !== null) { - this._kanbn.getTask(this._taskId).then(task => { + this._kanbn.getTask(this._taskId).then((task) => { this._panel.title = task.name; }); } @@ -142,58 +141,61 @@ export default class KanbnTaskPanel { this._panel.onDidDispose(() => this.dispose(), null, this._disposables); // Handle messages from the webview - this._panel.webview.onDidReceiveMessage(async message => { - switch (message.command) { + this._panel.webview.onDidReceiveMessage( + async (message) => { + switch (message.command) { + // Display error message + case "error": + vscode.window.showErrorMessage(message.text); + return; - // Display error message - case 'error': - vscode.window.showErrorMessage(message.text); - return; + // Update the task webview panel title + case "kanbn.updatePanelTitle": + this._panel.title = message.title; + return; - // Update the task webview panel title - case 'kanbn.updatePanelTitle': - this._panel.title = message.title; - return; - - // Create a task - case 'kanbn.create': - await this._kanbn.createTask(transformTaskData(message.taskData), message.taskData.column); - KanbnTaskPanel.panels[message.panelUuid]._taskId = message.taskData.id; - KanbnTaskPanel.panels[message.panelUuid]._columnName = message.taskData.column; - KanbnTaskPanel.panels[message.panelUuid].update(); - if (vscode.workspace.getConfiguration('kanbn').get('showTaskNotifications')) { - vscode.window.showInformationMessage(`Created task '${message.taskData.name}'.`); - } - return; - - // Update a task - case 'kanbn.update': - await this._kanbn.updateTask(message.taskId, transformTaskData(message.taskData), message.taskData.column); - KanbnTaskPanel.panels[message.panelUuid]._taskId = message.taskData.id; - KanbnTaskPanel.panels[message.panelUuid]._columnName = message.taskData.column; - KanbnTaskPanel.panels[message.panelUuid].update(); - if (vscode.workspace.getConfiguration('kanbn').get('showTaskNotifications')) { - vscode.window.showInformationMessage(`Updated task '${message.taskData.name}'.`); - } - return; - - // Delete a task and close the webview panel - case 'kanbn.delete': - vscode.window.showInformationMessage(`Delete task '${message.taskData.name}'?`, 'Yes', 'No').then( - async value => { - if (value === 'Yes') { - await this._kanbn.deleteTask(message.taskId, true); - KanbnTaskPanel.panels[message.panelUuid].dispose(); - delete KanbnTaskPanel.panels[message.panelUuid]; - if (vscode.workspace.getConfiguration('kanbn').get('showTaskNotifications')) { - vscode.window.showInformationMessage(`Deleted task '${message.taskData.name}'.`); - } - } + // Create a task + case "kanbn.create": + await this._kanbn.createTask(transformTaskData(message.taskData), message.taskData.column); + KanbnTaskPanel.panels[message.panelUuid]._taskId = message.taskData.id; + KanbnTaskPanel.panels[message.panelUuid]._columnName = message.taskData.column; + KanbnTaskPanel.panels[message.panelUuid].update(); + if (vscode.workspace.getConfiguration("kanbn").get("showTaskNotifications")) { + vscode.window.showInformationMessage(`Created task '${message.taskData.name}'.`); } - ); - return; - } - }, null, this._disposables); + return; + + // Update a task + case "kanbn.update": + await this._kanbn.updateTask(message.taskId, transformTaskData(message.taskData), message.taskData.column); + KanbnTaskPanel.panels[message.panelUuid]._taskId = message.taskData.id; + KanbnTaskPanel.panels[message.panelUuid]._columnName = message.taskData.column; + KanbnTaskPanel.panels[message.panelUuid].update(); + if (vscode.workspace.getConfiguration("kanbn").get("showTaskNotifications")) { + vscode.window.showInformationMessage(`Updated task '${message.taskData.name}'.`); + } + return; + + // Delete a task and close the webview panel + case "kanbn.delete": + vscode.window + .showInformationMessage(`Delete task '${message.taskData.name}'?`, "Yes", "No") + .then(async (value) => { + if (value === "Yes") { + await this._kanbn.deleteTask(message.taskId, true); + KanbnTaskPanel.panels[message.panelUuid].dispose(); + delete KanbnTaskPanel.panels[message.panelUuid]; + if (vscode.workspace.getConfiguration("kanbn").get("showTaskNotifications")) { + vscode.window.showInformationMessage(`Deleted task '${message.taskData.name}'.`); + } + } + }); + return; + } + }, + null, + this._disposables + ); } public dispose() { @@ -216,19 +218,17 @@ export default class KanbnTaskPanel { } let tasks: any[]; try { - tasks = (await this._kanbn.loadAllTrackedTasks(index)).map( - task => ({ - uuid: uuidv4(), - ...this._kanbn.hydrateTask(index, task) - }) - ); + tasks = (await this._kanbn.loadAllTrackedTasks(index)).map((task) => ({ + uuid: uuidv4(), + ...this._kanbn.hydrateTask(index, task), + })); } catch (error) { vscode.window.showErrorMessage(error instanceof Error ? error.message : error); return; } let task = null; if (this._taskId) { - task = tasks.find(t => t.id === this._taskId) ?? null; + task = tasks.find((t) => t.id === this._taskId) ?? null; } // If no columnName is specified, use the first column @@ -238,32 +238,32 @@ export default class KanbnTaskPanel { // Send task data to the webview this._panel.webview.postMessage({ - type: 'task', + type: "task", index, task, tasks, columnName: this._columnName, dateFormat: this._kanbn.getDateFormat(index), - panelUuid: this._panelUuid + panelUuid: this._panelUuid, }); } private _getHtmlForWebview() { - const manifest = require(path.join(this._extensionPath, 'build', 'asset-manifest.json')); - const mainScript = manifest['main.js']; - const mainStyle = manifest['main.css']; - const scriptUri = vscode.Uri - .file(path.join(this._extensionPath, 'build', mainScript)) - .with({ scheme: 'vscode-resource' }); - const styleUri = vscode.Uri - .file(path.join(this._extensionPath, 'build', mainStyle)) - .with({ scheme: 'vscode-resource' }); - const customStyleUri = vscode.Uri - .file(path.join(this._workspacePath, this._kanbn.getFolderName(), 'board.css')) - .with({ scheme: 'vscode-resource' }); - const codiconsUri = vscode.Uri - .file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css')) - .with({ scheme: 'vscode-resource' }); + const manifest = require(path.join(this._extensionPath, "build", "asset-manifest.json")); + const mainScript = manifest["main.js"]; + const mainStyle = manifest["main.css"]; + const scriptUri = vscode.Uri.file(path.join(this._extensionPath, "build", mainScript)).with({ + scheme: "vscode-resource", + }); + const styleUri = vscode.Uri.file(path.join(this._extensionPath, "build", mainStyle)).with({ + scheme: "vscode-resource", + }); + const customStyleUri = vscode.Uri.file( + path.join(this._workspacePath, this._kanbn.getFolderName(), "board.css") + ).with({ scheme: "vscode-resource" }); + const codiconsUri = vscode.Uri.file( + path.join(this._extensionPath, "node_modules", "vscode-codicons", "dist", "codicon.css") + ).with({ scheme: "vscode-resource" }); // Use a nonce to whitelist which scripts can be run const nonce = getNonce(); @@ -279,7 +279,7 @@ export default class KanbnTaskPanel { - + @@ -289,12 +289,3 @@ export default class KanbnTaskPanel { `; } } - -function getNonce() { - let text = ""; - const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} diff --git a/ext-src/extension.ts b/ext-src/extension.ts index 1618854..6b81d18 100644 --- a/ext-src/extension.ts +++ b/ext-src/extension.ts @@ -1,141 +1,129 @@ -import * as vscode from 'vscode'; -import KanbnStatusBarItem from './KanbnStatusBarItem'; -import KanbnBoardPanel from './KanbnBoardPanel'; -import KanbnBurndownPanel from './KanbnBurndownPanel'; -import KanbnTaskPanel from './KanbnTaskPanel'; +import * as vscode from "vscode"; +import KanbnStatusBarItem from "./KanbnStatusBarItem"; +import KanbnBoardPanel from "./KanbnBoardPanel"; +import KanbnBurndownPanel from "./KanbnBurndownPanel"; +import KanbnTaskPanel from "./KanbnTaskPanel"; let kanbnStatusBarItem: KanbnStatusBarItem; export async function activate(context: vscode.ExtensionContext) { - // Register a command to initialise Kanbn in the current workspace. This command will be invoked when the status // bar item is clicked in a workspace where Kanbn isn't already initialised. - context.subscriptions.push(vscode.commands.registerCommand('kanbn.init', async () => { - - // If no workspace folder is opened, we can't initialise kanbn - if (vscode.workspace.workspaceFolders === undefined) { - vscode.window.showErrorMessage('You need to open a workspace before initialising Kanbn.'); - return; - } - - // Set the node process directory and import kanbn - process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); - const kanbn = await import('@basementuniverse/kanbn/src/main'); - - // If kanbn is already initialised, get the project name - let projectName = ''; - if (await kanbn.initialised()) { - projectName = (await kanbn.getIndex()).name; - } - - // Prompt for a new project name - const newProjectName = await vscode.window.showInputBox({ - value: projectName, - placeHolder: 'The project name.', - validateInput: text => { - return text.length < 1 ? 'The project name cannot be empty.' : null; + context.subscriptions.push( + vscode.commands.registerCommand("kanbn.init", async () => { + // If no workspace folder is opened, we can't initialise kanbn + if (vscode.workspace.workspaceFolders === undefined) { + vscode.window.showErrorMessage("You need to open a workspace before initialising Kanbn."); + return; } - }); - // If the input prompt wasn't cancelled, initialise kanbn - if (newProjectName !== undefined) { - await kanbn.initialise({ - name: newProjectName + // Set the node process directory and import kanbn + process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); + const kanbn = await import("@basementuniverse/kanbn/src/main"); + + // If kanbn is already initialised, get the project name + let projectName = ""; + if (await kanbn.initialised()) { + projectName = (await kanbn.getIndex()).name; + } + + // Prompt for a new project name + const newProjectName = await vscode.window.showInputBox({ + value: projectName, + placeHolder: "The project name.", + validateInput: (text) => { + return text.length < 1 ? "The project name cannot be empty." : null; + }, }); - vscode.window.showInformationMessage(`Initialised Kanbn project '${newProjectName}'.`); - KanbnBoardPanel.update(); - } - kanbnStatusBarItem.update(); - })); + + // If the input prompt wasn't cancelled, initialise kanbn + if (newProjectName !== undefined) { + await kanbn.initialise({ + name: newProjectName, + }); + vscode.window.showInformationMessage(`Initialised Kanbn project '${newProjectName}'.`); + KanbnBoardPanel.update(); + } + kanbnStatusBarItem.update(); + }) + ); // Register a command to open the kanbn board. This command will be invoked when the status bar item is clicked // in a workspace where kanbn has already been initialised. - context.subscriptions.push(vscode.commands.registerCommand('kanbn.board', async () => { + context.subscriptions.push( + vscode.commands.registerCommand("kanbn.board", async () => { + // If no workspace folder is opened, we can't open the kanbn board + if (vscode.workspace.workspaceFolders === undefined) { + vscode.window.showErrorMessage("You need to open a workspace before viewing the Kanbn board."); + return; + } - // If no workspace folder is opened, we can't open the kanbn board - if (vscode.workspace.workspaceFolders === undefined) { - vscode.window.showErrorMessage('You need to open a workspace before viewing the Kanbn board.'); - return; - } + // Set the node process directory and import kanbn + process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); + const kanbn = await import("@basementuniverse/kanbn/src/main"); - // Set the node process directory and import kanbn - process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); - const kanbn = await import('@basementuniverse/kanbn/src/main'); - - // If kanbn is initialised, view the kanbn board - if (await kanbn.initialised()) { - KanbnBoardPanel.createOrShow( - context.extensionPath, - vscode.workspace.workspaceFolders[0].uri.fsPath, - kanbn - ); - KanbnBoardPanel.update(); - } else { - vscode.window.showErrorMessage('You need to initialise Kanbn before viewing the Kanbn board.'); - } - kanbnStatusBarItem.update(); - })); + // If kanbn is initialised, view the kanbn board + if (await kanbn.initialised()) { + KanbnBoardPanel.createOrShow(context.extensionPath, vscode.workspace.workspaceFolders[0].uri.fsPath, kanbn); + KanbnBoardPanel.update(); + } else { + vscode.window.showErrorMessage("You need to initialise Kanbn before viewing the Kanbn board."); + } + kanbnStatusBarItem.update(); + }) + ); // Register a command to add a new kanbn task. - context.subscriptions.push(vscode.commands.registerCommand('kanbn.addTask', async () => { + context.subscriptions.push( + vscode.commands.registerCommand("kanbn.addTask", async () => { + // If no workspace folder is opened, we can't add a new task + if (vscode.workspace.workspaceFolders === undefined) { + vscode.window.showErrorMessage("You need to open a workspace before adding a new task."); + return; + } - // If no workspace folder is opened, we can't add a new task - if (vscode.workspace.workspaceFolders === undefined) { - vscode.window.showErrorMessage('You need to open a workspace before adding a new task.'); - return; - } + // Set the node process directory and import kanbn + process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); + const kanbn = await import("@basementuniverse/kanbn/src/main"); - // Set the node process directory and import kanbn - process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); - const kanbn = await import('@basementuniverse/kanbn/src/main'); - - // If kanbn is initialised, open the task webview - if (await kanbn.initialised()) { - KanbnTaskPanel.show( - context.extensionPath, - vscode.workspace.workspaceFolders[0].uri.fsPath, - kanbn, - null, - null - ); - } else { - vscode.window.showErrorMessage('You need to initialise Kanbn before adding a new task.'); - } - })); + // If kanbn is initialised, open the task webview + if (await kanbn.initialised()) { + KanbnTaskPanel.show(context.extensionPath, vscode.workspace.workspaceFolders[0].uri.fsPath, kanbn, null, null); + } else { + vscode.window.showErrorMessage("You need to initialise Kanbn before adding a new task."); + } + }) + ); // Register a command to open a burndown chart. - context.subscriptions.push(vscode.commands.registerCommand('kanbn.burndown', async () => { + context.subscriptions.push( + vscode.commands.registerCommand("kanbn.burndown", async () => { + // If no workspace folder is opened, we can't open the burndown chart + if (vscode.workspace.workspaceFolders === undefined) { + vscode.window.showErrorMessage("You need to open a workspace before viewing the burndown chart."); + return; + } - // If no workspace folder is opened, we can't open the burndown chart - if (vscode.workspace.workspaceFolders === undefined) { - vscode.window.showErrorMessage('You need to open a workspace before viewing the burndown chart.'); - return; - } + // Set the node process directory and import kanbn + process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); + const kanbn = await import("@basementuniverse/kanbn/src/main"); - // Set the node process directory and import kanbn - process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); - const kanbn = await import('@basementuniverse/kanbn/src/main'); - - // If kanbn is initialised, view the burndown chart - if (await kanbn.initialised()) { - KanbnBurndownPanel.createOrShow( - context.extensionPath, - vscode.workspace.workspaceFolders[0].uri.fsPath, - kanbn - ); - KanbnBurndownPanel.update(); - } else { - vscode.window.showErrorMessage('You need to initialise Kanbn before viewing the burndown chart.'); - } - kanbnStatusBarItem.update(); - })); + // If kanbn is initialised, view the burndown chart + if (await kanbn.initialised()) { + KanbnBurndownPanel.show(context.extensionPath, vscode.workspace.workspaceFolders[0].uri.fsPath, kanbn); + KanbnBurndownPanel.updateAll(); + } else { + vscode.window.showErrorMessage("You need to initialise Kanbn before viewing the burndown chart."); + } + kanbnStatusBarItem.update(); + }) + ); // If a workspace folder is open, add a status bar item and start watching for file changes if (vscode.workspace.workspaceFolders !== undefined) { - // Set the node process directory and import kanbn process.chdir(vscode.workspace.workspaceFolders[0].uri.fsPath); - const kanbn = await import('@basementuniverse/kanbn/src/main'); + const kanbn = await import("@basementuniverse/kanbn/src/main"); // Create status bar item kanbnStatusBarItem = new KanbnStatusBarItem(context, kanbn); @@ -144,18 +132,18 @@ export async function activate(context: vscode.ExtensionContext) { // Initialise file watcher const uri = vscode.workspace.workspaceFolders[0].uri.fsPath; - const fileWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern( - uri, - `${kanbn.getFolderName()}/**.*` - )); + const fileWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(uri, `${kanbn.getFolderName()}/**.*`) + ); fileWatcher.onDidChange(() => { kanbnStatusBarItem.update(); KanbnBoardPanel.update(); + KanbnBurndownPanel.updateAll(); }); } // Handle configuration changes - vscode.workspace.onDidChangeConfiguration(e => { + vscode.workspace.onDidChangeConfiguration((e) => { kanbnStatusBarItem.update(); KanbnBoardPanel.update(); }); diff --git a/ext-src/getNonce.ts b/ext-src/getNonce.ts new file mode 100644 index 0000000..8f70aa4 --- /dev/null +++ b/ext-src/getNonce.ts @@ -0,0 +1,8 @@ +export default function getNonce() { + let text = ""; + const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/src/App.tsx b/src/App.tsx index 7143b9a..debd8ab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,9 @@ function App() { const [burndownData, setBurndownData] = useState({ series: [] }); window.addEventListener('message', event => { - const tasks = Object.fromEntries(event.data.tasks.map(task => [task.id, task])); + const tasks = event.data.tasks + ? Object.fromEntries(event.data.tasks.map(task => [task.id, task])) + : {}; switch (event.data.type) { case 'index': setName(event.data.index.name); @@ -66,7 +68,11 @@ function App() { case 'burndown': setName(event.data.index.name); setTasks(tasks); - setSprints(event.data.index.options.sprints || []); + setSprints( + 'sprints' in event.data.index.options + ? event.data.index.options.sprints + : [] + ); setBurndownData(event.data.burndownData); break; } @@ -111,6 +117,7 @@ function App() { tasks={tasks} sprints={sprints} burndownData={burndownData} + vscode={vscode} /> } diff --git a/src/Burndown.tsx b/src/Burndown.tsx index 8bdf6c2..304b6eb 100644 --- a/src/Burndown.tsx +++ b/src/Burndown.tsx @@ -1,7 +1,9 @@ -import React from 'react'; -import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts'; +import React, { useState } from 'react'; +import { ResponsiveContainer, LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts'; +import VSCodeApi from "./VSCodeApi"; +import formatDate from 'dateformat'; -const Burndown = ({ name, tasks, sprints, burndownData }: { +const Burndown = ({ name, tasks, sprints, burndownData, vscode }: { name: string, tasks: Record, sprints: KanbnSprint[], @@ -15,8 +17,14 @@ const Burndown = ({ name, tasks, sprints, burndownData }: { y: number }> }> - } + }, + vscode: VSCodeApi }) => { + const [sprintMode, setSprintMode] = useState(sprints.length > 0); + const [sprint, setSprint] = useState(null); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); + const data = [ {name: 'one', uv: 20}, {name: 'two', uv: 80}, @@ -26,14 +34,82 @@ const Burndown = ({ name, tasks, sprints, burndownData }: { return ( -
burndown chart
- - - - - - - +
+

+

{name}

+
+
+ { + sprintMode + ? + : + { console.log(e); }} + /> + { console.log(e); }} + /> + + } + + +
+
+

+
+
+ + + + + + + + + +
); }; diff --git a/src/index.css b/src/index.css index 68c1660..d7257e8 100644 --- a/src/index.css +++ b/src/index.css @@ -20,7 +20,7 @@ body.vscode-high-contrast { .kanbn-header-name p { display: inline-block; - flex: 1; + flex: 2; margin: 0; padding: 4px 0; } @@ -42,6 +42,8 @@ body.vscode-high-contrast { padding: 8px; background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); border: 1px transparent solid; } @@ -340,12 +342,13 @@ body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-p margin: 8px 0; background-color: var(--vscode-input-background); color: var(--vscode-input-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); border: 1px transparent solid; } .kanbn-task-editor-field-input[type=date] { - font-family: var(--vscode-font-family); - font-size: var(--vscode-font-size); + padding: 6px 8px; } .kanbn-task-editor-field-select { @@ -477,3 +480,60 @@ body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-p .kanbn-task-editor-field-comment-text .kanbn-task-editor-field-textarea { min-height: 90px; } + +.kanbn-burndown-settings { + position: relative; + flex: 1; +} + +.kanbn-burndown-settings form { + width: 100%; + display: flex; + white-space: nowrap; +} + +.kanbn-burndown-settings-sprint-select { + box-sizing: border-box; + flex: 1; + padding: 8px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px transparent solid; +} + +.kanbn-burndown-settings-input { + box-sizing: border-box; + display: block; + flex: 0.5; + padding: 8px; + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + font-family: var(--vscode-font-family); + font-size: var(--vscode-font-size); + border: 1px transparent solid; +} + +.kanbn-burndown-settings-input[type=date] { + padding: 6px 8px; + margin-left: 8px; +} + +.kanbn-burndown-settings-sprint-select:hover, +.kanbn-burndown-settings-sprint-select:focus, +.kanbn-burndown-settings-input:hover, +.kanbn-burndown-settings-input:focus { + border-color: var(--vscode-input-border); +} + +body.vscode-dark .kanbn-burndown-settings-input[type="date"]::-webkit-calendar-picker-indicator { + filter: invert(1); +} + +.kanbn-header-button-inactive { + opacity: 0.6; +} + +.kanbn-burndown { + height: 85vh; + width: 95vw; +}