Allow opening multiple burndown panels

This commit is contained in:
Gordon 2021-04-17 01:28:17 +01:00
parent 0d8508dd19
commit cb8647ba5a
8 changed files with 515 additions and 405 deletions

View File

@ -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 {
<link rel="stylesheet" type="text/css" href="${customStyleUri}">
<link rel="stylesheet" type="text/css" href="${codiconsUri}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; font-src vscode-resource:; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${vscode.Uri.file(path.join(this._extensionPath, 'build')).with({ scheme: 'vscode-resource' })}/">
<base href="${vscode.Uri.file(path.join(this._extensionPath, "build")).with({ scheme: "vscode-resource" })}/">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@ -232,12 +225,3 @@ export default class KanbnBoardPanel {
</html>`;
}
}
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;
}

View File

@ -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<string, KanbnBurndownPanel> = {};
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 {
</html>`;
}
}
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;
}

View File

@ -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<string, KanbnTaskPanel> = {};
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 {
<link rel="stylesheet" type="text/css" href="${customStyleUri}">
<link rel="stylesheet" type="text/css" href="${codiconsUri}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; font-src vscode-resource:; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${vscode.Uri.file(path.join(this._extensionPath, 'build')).with({ scheme: 'vscode-resource' })}/">
<base href="${vscode.Uri.file(path.join(this._extensionPath, "build")).with({ scheme: "vscode-resource" })}/">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
@ -289,12 +289,3 @@ export default class KanbnTaskPanel {
</html>`;
}
}
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;
}

View File

@ -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();
});

8
ext-src/getNonce.ts Normal file
View File

@ -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;
}

View File

@ -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}
/>
}
</React.Fragment>

View File

@ -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<string, KanbnTask>,
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 (
<React.Fragment>
<div>burndown chart</div>
<LineChart width={600} height={300} data={data} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
<CartesianGrid stroke="#ccc" strokeDasharray="5 5" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
</LineChart>
<div className="kanbn-header">
<h1 className="kanbn-header-name">
<p>{name}</p>
<div className="kanbn-burndown-settings">
<form>
{
sprintMode
? <select className="kanbn-burndown-settings-sprint-select" onChange={e => { console.log(e); }}>
{
sprints.length > 0
? sprints.map((sprint, i) => {
return (
<option value={i}>{sprint.name}</option>
);
})
: <option disabled>No sprints</option>
}
</select>
: <React.Fragment>
<input
type="date"
className="kanbn-burndown-settings-input kanbn-burndown-settings-start-date"
onChange={e => { console.log(e); }}
/>
<input
type="date"
className="kanbn-burndown-settings-input kanbn-burndown-settings-end-date"
onChange={e => { console.log(e); }}
/>
</React.Fragment>
}
<button
type="button"
className={[
'kanbn-header-button',
'kanbn-burndown-settings-sprint-mode',
sprintMode ? 'kanbn-header-button-active' : 'kanbn-header-button-inactive'
].join(' ')}
onClick={() => {
setSprintMode(true);
// update burndown chart
}}
title="View sprint burndown"
>
<i className="codicon codicon-rocket"></i>
</button>
<button
type="button"
className={[
'kanbn-header-button',
'kanbn-burndown-settings-date-mode',
sprintMode ? 'kanbn-header-button-inactive' : 'kanbn-header-button-active'
].join(' ')}
onClick={() => {
setSprintMode(false);
// update burndown chart
}}
title="View date-range burndown"
>
<i className="codicon codicon-clock"></i>
</button>
</form>
</div>
</h1>
</div>
<div className="kanbn-burndown">
<ResponsiveContainer width="100%" height="100%" className="kanbn-burndown-chart">
<LineChart data={data}>
<Line type="monotone" dataKey="uv" stroke="#8884d8" />
<CartesianGrid stroke="#ccc" strokeDasharray="5 5" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
</LineChart>
</ResponsiveContainer>
</div>
</React.Fragment>
);
};

View File

@ -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;
}