From 708b13005988db4e7856d0d1213e5608005bfb5d Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 2 Apr 2021 16:14:58 +0100 Subject: [PATCH] Open or create tasks in a new webview panel --- ext-src/KanbnBoardPanel.ts | 46 ++++++++++---------- ext-src/KanbnTaskPanel.ts | 70 +++++++++++++++++++++++++++---- ext-src/extension.ts | 5 ++- src/App.tsx | 77 ++++++++++++++++++++++++---------- src/Board.tsx | 22 +++++----- src/KanbnTask.d.ts | 1 + src/TaskEditor.tsx | 19 +++++++++ src/{Task.tsx => TaskItem.tsx} | 9 ++-- src/index.css | 6 ++- 9 files changed, 184 insertions(+), 71 deletions(-) create mode 100644 src/TaskEditor.tsx rename src/{Task.tsx => TaskItem.tsx} (93%) diff --git a/ext-src/KanbnBoardPanel.ts b/ext-src/KanbnBoardPanel.ts index 6591894..04f4343 100644 --- a/ext-src/KanbnBoardPanel.ts +++ b/ext-src/KanbnBoardPanel.ts @@ -43,6 +43,7 @@ export default class KanbnBoardPanel { return; } KanbnBoardPanel.currentPanel._panel.webview.postMessage({ + type: 'index', index, tasks: (await KanbnBoardPanel.currentPanel._kanbn.loadAllTrackedTasks(index)).map( task => KanbnBoardPanel.currentPanel!._kanbn.hydrateTask(index, task) @@ -75,10 +76,14 @@ 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._workspacePath, '.kanbn')), + vscode.Uri.file(path.join(this._workspacePath, this._kanbn.getFolderName())), vscode.Uri.file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist')) ] }); + this._panel.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')) + }; // Set the webview's title to the kanbn project name this._kanbn.getIndex().then(index => { @@ -102,19 +107,19 @@ export default class KanbnBoardPanel { return; case 'kanbn.task': - // KanbnTaskPanel.create( - // this._extensionPath, - // this._workspacePath, - // this._kanbn, - // message.taskId - // ); - vscode.window.showInformationMessage(`Opening task ${message.taskId}`); + 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.column, message.position); + await kanbn.moveTask(message.task, message.columnName, message.position); } catch (e) { vscode.window.showErrorMessage(e.message); } @@ -122,20 +127,13 @@ export default class KanbnBoardPanel { // Create a task case 'kanbn.create': - // TODO open task panel with blank task - vscode.window.showInformationMessage(`Creating new task in column ${message.column}`); - return; - - // Update a task - case 'kanbn.update': - // TODO update task - vscode.window.showInformationMessage(`Editing task ${message.taskId}`); - return; - - // Delete a task - case 'kanbn.delete': - // TODO delete task - vscode.window.showInformationMessage(`Deleting task ${message.taskId}`); + KanbnTaskPanel.show( + this._extensionPath, + this._workspacePath, + this._kanbn, + null, + message.columnName + ); return; } }, null, this._disposables); @@ -165,7 +163,7 @@ export default class KanbnBoardPanel { .file(path.join(this._extensionPath, 'build', mainStyle)) .with({ scheme: 'vscode-resource' }); const customStyleUri = vscode.Uri - .file(path.join(this._workspacePath, '.kanbn', 'board.css')) + .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')) diff --git a/ext-src/KanbnTaskPanel.ts b/ext-src/KanbnTaskPanel.ts index 2388220..e6e1a7c 100644 --- a/ext-src/KanbnTaskPanel.ts +++ b/ext-src/KanbnTaskPanel.ts @@ -3,25 +3,77 @@ import * as vscode from 'vscode'; export default class KanbnTaskPanel { private static readonly viewType = 'react'; + private static panels: KanbnTaskPanel[] = []; private readonly _panel: vscode.WebviewPanel; private readonly _extensionPath: string; private readonly _workspacePath: string; private readonly _kanbn: typeof import('@basementuniverse/kanbn/src/main'); + private readonly _taskId: string|null; + private readonly _columnName: string; private _disposables: vscode.Disposable[] = []; + public static async show( + extensionPath: string, + workspacePath: string, + kanbn: typeof import('@basementuniverse/kanbn/src/main'), + taskId: string|null, + columnName: string + ) { + const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; + + // Create a new panel + const taskPanel = new KanbnTaskPanel( + extensionPath, + workspacePath, + column || vscode.ViewColumn.One, + kanbn, + taskId, + columnName + ); + KanbnTaskPanel.panels.push(taskPanel); + + let index: any; + try { + index = await kanbn.getIndex(); + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + return; + } + let task: any = null; + if (taskId) { + try { + task = await kanbn.getTask(taskId); + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + return; + } + } + taskPanel._panel.webview.postMessage({ + type: 'task', + task, + columnName: taskPanel._columnName, + tasks: await kanbn.loadAllTrackedTasks(index), + dateFormat: kanbn.getDateFormat(index) + }); + } + private constructor( extensionPath: string, workspacePath: string, column: vscode.ViewColumn, - kanbn: typeof import('@basementuniverse/kanbn/src/main') + kanbn: typeof import('@basementuniverse/kanbn/src/main'), + taskId: string|null, + columnName: string ) { this._extensionPath = extensionPath; this._workspacePath = workspacePath; this._kanbn = kanbn; + this._taskId = taskId; + this._columnName = columnName; // Create and show a new webview panel - this._panel = vscode.window.createWebviewPanel(KanbnTaskPanel.viewType, 'Kanbn Task', column, { + this._panel = vscode.window.createWebviewPanel(KanbnTaskPanel.viewType, 'New task', column, { // Enable javascript in the webview enableScripts: true, @@ -31,15 +83,17 @@ 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._workspacePath, '.kanbn')), + vscode.Uri.file(path.join(this._workspacePath, this._kanbn.getFolderName())), vscode.Uri.file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist')) ] }); // Set the webview's title to the kanbn task name - // this._kanbn.getIndex().then(index => { - // this._panel.title = index.name; - // }); + if (this._taskId !== null) { + this._kanbn.getTask(this._taskId).then(task => { + this._panel.title = task.name; + }); + } // Set the webview's initial html content this._panel.webview.html = this._getHtmlForWebview(); @@ -60,7 +114,7 @@ export default class KanbnTaskPanel { // Update a task case 'kanbn.update': // TODO update task - vscode.window.showInformationMessage(`Editing task ${message.taskId}`); + vscode.window.showInformationMessage(`Updating task ${message.taskId}`); return; // Delete a task @@ -93,7 +147,7 @@ export default class KanbnTaskPanel { .file(path.join(this._extensionPath, 'build', mainStyle)) .with({ scheme: 'vscode-resource' }); const customStyleUri = vscode.Uri - .file(path.join(this._workspacePath, '.kanbn', 'board.css')) + .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')) diff --git a/ext-src/extension.ts b/ext-src/extension.ts index 787f240..0042764 100644 --- a/ext-src/extension.ts +++ b/ext-src/extension.ts @@ -87,7 +87,10 @@ 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/*')); + const fileWatcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern( + uri, + `${kanbn.getFolderName()}/**.*` + )); fileWatcher.onDidChange(() => { kanbnStatusBarItem.update(); KanbnBoardPanel.update(); diff --git a/src/App.tsx b/src/App.tsx index 53aeca9..33a7154 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,44 +1,77 @@ -import Board from './Board'; import Header from './Header'; +import Board from './Board'; +import TaskEditor from './TaskEditor'; import React, { useState } from "react"; +import VSCodeApi from "./VSCodeApi"; + +declare var acquireVsCodeApi: Function; +const vscode: VSCodeApi = acquireVsCodeApi(); const zip = (a: Array, b: Array): Array<[any, any]> => a.map((v: any, i: number): [any, any] => [v, b[i]]); function App() { + const [type, setType] = useState(''); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [columns, setColumns] = useState({}); const [startedColumns, setStartedColumns] = useState([]); const [completedColumns, setCompletedColumns] = useState([]); const [dateFormat, setDateFormat] = useState(''); + const [task, setTask] = useState({}); + const [columnName, setColumnName] = useState(''); window.addEventListener('message', event => { - const tasks = Object.fromEntries(event.data.tasks.map(task => [task.id, task])); - setName(event.data.index.name); - setDescription(event.data.index.description); - setColumns(Object.fromEntries( - zip( - Object.keys(event.data.index.columns), - Object.values(event.data.index.columns).map(column => (column as string[]).map(taskId => tasks[taskId])) - ) - )); - setStartedColumns(event.data.startedColumns); - setCompletedColumns(event.data.completedColumns); + switch (event.data.type) { + case 'index': + const tasks = Object.fromEntries(event.data.tasks.map(task => [task.id, task])); + setName(event.data.index.name); + setDescription(event.data.index.description); + setColumns(Object.fromEntries( + zip( + Object.keys(event.data.index.columns), + Object.values(event.data.index.columns).map(column => (column as string[]).map(taskId => tasks[taskId])) + ) + )); + setStartedColumns(event.data.startedColumns); + setCompletedColumns(event.data.completedColumns); + break; + + case 'task': + setTask(event.data.task); + setColumnName(event.data.columnName); + break; + } + setType(event.data.type); setDateFormat(event.data.dateFormat); }); return (
-
- + { + type === 'index' && + +
+ + + } + { + type === 'task' && + + }
); } diff --git a/src/Board.tsx b/src/Board.tsx index c3fab94..f84dd23 100644 --- a/src/Board.tsx +++ b/src/Board.tsx @@ -1,13 +1,10 @@ import { DragDropContext, Droppable } from "react-beautiful-dnd"; import React, { useState } from "react"; -import Task from './Task'; -import VSCodeApi from "./VSCodeApi"; +import TaskItem from './TaskItem'; import { paramCase } from 'param-case'; +import VSCodeApi from "./VSCodeApi"; -declare var acquireVsCodeApi: Function; -const vscode: VSCodeApi = acquireVsCodeApi(); - -const onDragEnd = (result, columns, setColumns) => { +const onDragEnd = (result, columns, setColumns, vscode: VSCodeApi) => { // No destination means the item was dragged to an invalid location if (!result.destination) { @@ -53,16 +50,17 @@ const onDragEnd = (result, columns, setColumns) => { vscode.postMessage({ command: 'kanbn.move', task: removed.id, - column: destination.droppableId, + columnName: destination.droppableId, position: destination.index }); }; -const Board = ({ columns, startedColumns, completedColumns, dateFormat }: { +const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }: { columns: Record, startedColumns: string[], completedColumns: string[], - dateFormat: string + dateFormat: string, + vscode: VSCodeApi }) => { const [, setColumns] = useState(columns); return ( @@ -73,7 +71,7 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat }: { }} > onDragEnd(result, columns, setColumns)} + onDragEnd={result => onDragEnd(result, columns, setColumns, vscode)} > {Object.entries(columns).map(([columnName, column]) => { return ( @@ -104,7 +102,7 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat }: { onClick={() => { vscode.postMessage({ command: 'kanbn.create', - column: columnName + columnName }) }} title={`Create task in ${columnName}`} @@ -124,7 +122,7 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat }: { snapshot.isDraggingOver ? 'drag-over' : null ].filter(i => i).join(' ')} > - {column.map((task, index) => { + return ( +
+ Viewing or editing task: {task ? task.name : '(creating new task)'}
+ Column: {columnName} +
+ ); +} + +export default TaskEditor; diff --git a/src/Task.tsx b/src/TaskItem.tsx similarity index 93% rename from src/Task.tsx rename to src/TaskItem.tsx index 31e8dd1..950b5de 100644 --- a/src/Task.tsx +++ b/src/TaskItem.tsx @@ -3,7 +3,7 @@ import { Draggable } from "react-beautiful-dnd"; import formatDate from 'dateformat'; import VSCodeApi from "./VSCodeApi"; -const Task = ({ task, index, dateFormat, vscode }: { +const TaskItem = ({ task, index, dateFormat, vscode }: { task: KanbnTask, index: number, dateFormat: string, @@ -12,6 +12,7 @@ const Task = ({ task, index, dateFormat, vscode }: { const createdDate = 'created' in task.metadata ? formatDate(task.metadata.created, dateFormat) : ''; const updatedDate = 'updated' in task.metadata ? formatDate(task.metadata.updated, dateFormat) : ''; const startedDate = 'started' in task.metadata ? formatDate(task.metadata.started, dateFormat) : ''; + const dueDate = 'due' in task.metadata ? formatDate(task.metadata.due, dateFormat) : ''; const completedDate = 'completed' in task.metadata ? formatDate(task.metadata.completed, dateFormat) : ''; return ( { vscode.postMessage({ command: 'kanbn.task', - taskId: task.id + taskId: task.id, + columnName: task.column }) }} title={task.id} @@ -79,6 +81,7 @@ const Task = ({ task, index, dateFormat, vscode }: { createdDate ? `Created ${createdDate}` : null, updatedDate ? `Updated ${updatedDate}` : null, startedDate ? `Started ${startedDate}` : null, + dueDate ? `Due ${dueDate}` : null, completedDate ? `Completed ${completedDate}` : null ].filter(i => i).join('\n')}> {updatedDate || createdDate} @@ -118,4 +121,4 @@ const Task = ({ task, index, dateFormat, vscode }: { ); } -export default Task; +export default TaskItem; diff --git a/src/index.css b/src/index.css index 9f318da..b3343ac 100644 --- a/src/index.css +++ b/src/index.css @@ -41,6 +41,8 @@ body { height: 16px; width: 16px; margin-right: 8px; + cursor: pointer; + border-radius: 1px; } .kanbn-create-task-button:focus, .kanbn-create-task-button:hover { @@ -127,6 +129,7 @@ body { background-color: transparent; font-weight: bold; color: var(--vscode-editor-foreground); + cursor: pointer; } .kanbn-task-name:focus, .kanbn-task-name:hover { @@ -156,10 +159,11 @@ body { display: inline-block; background-color: var(--vscode-activityBar-inactiveForeground); color: var(--vscode-editor-background); + font-size: 0.8em; font-weight: bold; padding: 2px 4px; margin: 4px 4px 4px 0; - border-radius: 2px; + border-radius: 4px; } .kanbn-task-tag-Nothing {