From a26d1ba7c037eff950aabd4711bcb191f1d1050d Mon Sep 17 00:00:00 2001 From: Gordon Date: Wed, 7 Apr 2021 21:25:57 +0100 Subject: [PATCH] Bugfixes --- ext-src/KanbnTaskPanel.ts | 177 +++++--- ext-src/extension.ts | 3 +- src/App.tsx | 3 + src/TaskEditor.tsx | 903 +++++++++++++++++++------------------- src/TaskItem.tsx | 2 + src/index.css | 6 + 6 files changed, 594 insertions(+), 500 deletions(-) diff --git a/ext-src/KanbnTaskPanel.ts b/ext-src/KanbnTaskPanel.ts index 8026216..379ecec 100644 --- a/ext-src/KanbnTaskPanel.ts +++ b/ext-src/KanbnTaskPanel.ts @@ -2,16 +2,69 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { v4 as uuidv4 } from 'uuid'; +function transformTaskData(taskData: any) { + const result = { + id: taskData.id, + name: taskData.name, + description: taskData.description, + metadata: { + 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 + } 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 + })) + } as any; + + // Add assigned + if (taskData.metadata.assigned) { + result.metadata['assigned'] = taskData.metadata.assigned; + } + + // Add progress + if (taskData.progress > 0) { + result.metadata['progress'] = taskData.progress; + } + + // Add tags + if (taskData.metadata.tags.length) { + 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)); + } + if (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)); + } + + return result; +} + export default class KanbnTaskPanel { private static readonly viewType = 'react'; - private static panels: KanbnTaskPanel[] = []; + 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 _taskId: string|null; - private readonly _columnName: string; + private _taskId: string|null; + private _columnName: string|null; + private readonly _panelUuid: string; private _disposables: vscode.Disposable[] = []; public static async show( @@ -22,55 +75,20 @@ export default class KanbnTaskPanel { columnName: string|null ) { const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; - 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 => ({ - uuid: uuidv4(), - ...kanbn.hydrateTask(index, task) - }) - ); - } catch (error) { - vscode.window.showErrorMessage(error instanceof Error ? error.message : error); - return; - } - let task = null; - if (taskId) { - task = tasks.find(t => t.id === taskId) ?? null; - } - - // If no columnName is specified, use the first column - if (!columnName) { - columnName = Object.keys(index.columns)[0]; - } // Create a new panel + const panelUuid = uuidv4(); const taskPanel = new KanbnTaskPanel( extensionPath, workspacePath, column || vscode.ViewColumn.One, kanbn, taskId, - columnName + columnName, + panelUuid ); - KanbnTaskPanel.panels.push(taskPanel); - - // Send task data to the webview - taskPanel._panel.webview.postMessage({ - type: 'task', - index, - task, - columnName: taskPanel._columnName, - tasks, - dateFormat: kanbn.getDateFormat(index) - }); + KanbnTaskPanel.panels[panelUuid] = taskPanel; + taskPanel.update(); } private constructor( @@ -79,13 +97,15 @@ export default class KanbnTaskPanel { column: vscode.ViewColumn, kanbn: typeof import('@basementuniverse/kanbn/src/main'), taskId: string|null, - columnName: string + columnName: string|null, + panelUuid: string ) { this._extensionPath = extensionPath; this._workspacePath = workspacePath; this._kanbn = kanbn; this._taskId = taskId; this._columnName = columnName; + this._panelUuid = panelUuid; // Create and show a new webview panel this._panel = vscode.window.createWebviewPanel(KanbnTaskPanel.viewType, 'New task', column, { @@ -137,26 +157,31 @@ export default class KanbnTaskPanel { // Create a task case 'kanbn.create': - // TODO convert dates - // await this._kanbn.createTask(message.taskData, message.taskData.column); - vscode.window.showInformationMessage(`Created task ${message.taskData.name}.`); + 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(); + // vscode.window.showInformationMessage(`Created task '${message.taskData.name}'.`); return; // Update a task case 'kanbn.update': - // TODO convert dates - // await this._kanbn.updateTask(message.taskData.id, message.taskData, message.taskData.column); - vscode.window.showInformationMessage(`Updated task ${message.taskData.name}.`); + 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(); + // vscode.window.showInformationMessage(`Updated task '${message.taskData.name}'.`); return; - // Delete a task + // Delete a task and close the webview panel case 'kanbn.delete': - vscode.window.showInformationMessage(`Delete task ${message.taskData.name}?`, 'Yes', 'No').then( + vscode.window.showInformationMessage(`Delete task '${message.taskData.name}'?`, 'Yes', 'No').then( async value => { if (value === 'Yes') { - // await this._kanbn.deleteTask(message.taskId, true); - vscode.window.showInformationMessage(`Deleted task ${message.taskData.name}.`); - // TODO close panel, will need to generate uuid for each panel + await this._kanbn.deleteTask(message.taskId, true); + KanbnTaskPanel.panels[message.panelUuid].dispose(); + delete KanbnTaskPanel.panels[message.panelUuid]; + // vscode.window.showInformationMessage(`Deleted task '${message.taskData.name}'.`); } } ); @@ -175,6 +200,48 @@ export default class KanbnTaskPanel { } } + private async update() { + let index: any; + try { + index = await this._kanbn.getIndex(); + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + return; + } + let tasks: any[]; + try { + 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; + } + + // If no columnName is specified, use the first column + if (!this._columnName) { + this._columnName = Object.keys(index.columns)[0]; + } + + // Send task data to the webview + this._panel.webview.postMessage({ + type: 'task', + index, + task, + tasks, + columnName: this._columnName, + dateFormat: this._kanbn.getDateFormat(index), + panelUuid: this._panelUuid + }); + } + private _getHtmlForWebview() { const manifest = require(path.join(this._extensionPath, 'build', 'asset-manifest.json')); const mainScript = manifest['main.js']; diff --git a/ext-src/extension.ts b/ext-src/extension.ts index 7697bf9..cbf78da 100644 --- a/ext-src/extension.ts +++ b/ext-src/extension.ts @@ -42,9 +42,9 @@ export async function activate(context: vscode.ExtensionContext) { name: newProjectName }); vscode.window.showInformationMessage(`Initialised kanbn project '${newProjectName}'.`); - kanbnStatusBarItem.update(); KanbnBoardPanel.update(); } + kanbnStatusBarItem.update(); })); // Register a command to open the kanbn board. This command will be invoked when the status bar item is clicked @@ -72,6 +72,7 @@ export async function activate(context: vscode.ExtensionContext) { } 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. diff --git a/src/App.tsx b/src/App.tsx index f14deee..ff9a352 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ function App() { const [tasks, setTasks] = useState({}); const [columnName, setColumnName] = useState(''); const [columnNames, setColumnNames] = useState([] as string[]); + const [panelUuid, setPanelUuid] = useState(''); window.addEventListener('message', event => { const tasks = Object.fromEntries(event.data.tasks.map(task => [task.id, task])); @@ -42,6 +43,7 @@ function App() { setTasks(tasks); setColumnName(event.data.columnName); setColumnNames(Object.keys(event.data.index.columns)); + setPanelUuid(event.data.panelUuid); break; } setType(event.data.type); @@ -70,6 +72,7 @@ function App() { columnName={columnName} columnNames={columnNames} dateFormat={dateFormat} + panelUuid={panelUuid} vscode={vscode} /> } diff --git a/src/TaskEditor.tsx b/src/TaskEditor.tsx index e9d2204..daef10a 100644 --- a/src/TaskEditor.tsx +++ b/src/TaskEditor.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Formik, Form, Field, ErrorMessage, FieldArray } from 'formik'; import formatDate from 'dateformat'; import VSCodeApi from './VSCodeApi'; @@ -19,19 +19,38 @@ interface KanbnTaskValidationOutput { } interface KanbnTaskValidationInput extends KanbnTaskValidationOutput { - uuid: string, id: string } -const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }: { +const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, panelUuid, vscode }: { task: KanbnTask|null, tasks: Record, columnName: string, columnNames: string[], dateFormat: string, + panelUuid: string, vscode: VSCodeApi }) => { const editing = task !== null; + const [taskData, setTaskData] = useState({ + id: task ? task.id : '', + name: task ? task.name : '', + description: task ? task.description : '', + column: columnName, + progress: task ? task.progress : 0, + metadata: { + created: (task && 'created' in task.metadata) ? task.metadata.created : new Date(), + updated: (task && 'updated' in task.metadata) ? task.metadata.updated : null, + started: (task && 'started' in task.metadata) ? formatDate(task.metadata.started!, 'yyyy-mm-dd') : '', + due: (task && 'due' in task.metadata) ? formatDate(task.metadata.due!, 'yyyy-mm-dd') : '', + completed: (task && 'completed' in task.metadata) ? formatDate(task.metadata.completed!, 'yyyy-mm-dd') : '', + assigned: (task && 'assigned' in task.metadata) ? task.metadata.assigned : (gitUsername() || ''), + tags: (task && 'tags' in task.metadata) ? (task.metadata.tags || []) : [] + }, + relations: task ? task.relations : [], + subTasks: task ? task.subTasks : [], + comments: task ? task.comments : [] + }); // Called when the name field is changed const handleUpdateName = ({ target: { value }}, values) => { @@ -47,18 +66,24 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode } }; // Called when the form is submitted - const handleSubmit = values => { + const handleSubmit = (values, setSubmitting, resetForm) => { if (editing) { vscode.postMessage({ command: 'kanbn.update', - taskData: values + taskId: task!.id, + taskData: values, + panelUuid }); } else { vscode.postMessage({ command: 'kanbn.create', - taskData: values + taskData: values, + panelUuid }); } + setTaskData(values); + resetForm({ values }); + setSubmitting(false); }; // Called when the delete task button is clicked @@ -66,7 +91,8 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode } vscode.postMessage({ command: 'kanbn.delete', taskId: task!.id, - taskData: values + taskData: values, + panelUuid }); }; @@ -78,501 +104,490 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode } return false; }; + // Validate form data + const validate = (values: KanbnTaskValidationInput): KanbnTaskValidationOutput|{} => { + let hasErrors = false; + const errors: KanbnTaskValidationOutput = { + name: '', + metadata: { + tags: [] + }, + subTasks: [], + comments: [] + }; + + // Task name cannot be empty + if (!values.name) { + errors.name = 'Task name is required.'; + hasErrors = true; + } + + // Check if the id is already in use + if (values.id in tasks && tasks[values.id].uuid !== (task ? task.uuid : '')) { + errors.name = 'There is already a task with the same name or id.'; + hasErrors = true; + } + + // Tag names cannot be empty + for (let i = 0; i < values.metadata.tags.length; i++) { + if (!values.metadata.tags[i]) { + errors.metadata.tags[i] = 'Tag cannot be empty.'; + hasErrors = true; + } + } + + // Sub-tasks text cannot be empty + for (let i = 0; i < values.subTasks.length; i++) { + if (!values.subTasks[i].text) { + errors.subTasks[i] = { + text: 'Sub-task text cannot be empty.' + }; + hasErrors = true; + } + } + + // Comments text cannot be empty + for (let i = 0; i < values.comments.length; i++) { + if (!values.comments[i].text) { + errors.comments[i] = { + text: 'Comment text cannot be empty.' + }; + hasErrors = true; + } + } + + return hasErrors ? errors : {}; + }; + return (
-

- {editing ? 'Update task' : 'Create new task'} - {editing && - { - [ - 'created' in task!.metadata ? `Created ${formatDate(task!.metadata.created, dateFormat)}` : null, - 'updated' in task!.metadata ? `Updated ${formatDate(task!.metadata.updated, dateFormat)}` : null - ].filter(i => i).join(', ') - } - } -

{ - let hasErrors = false; - const errors: KanbnTaskValidationOutput = { - name: '', - metadata: { - tags: [] - }, - subTasks: [], - comments: [] - }; - - // Task name cannot be empty - if (!values.name) { - errors.name = 'Task name is required.'; - hasErrors = true; - } - - // Check if the id is already in use - if (values.id in tasks && tasks[values.id].uuid !== values.uuid) { - errors.name = 'There is already a task with the same name or id.'; - hasErrors = true; - } - - // Tag names cannot be empty - for (let i = 0; i < values.metadata.tags.length; i++) { - if (!values.metadata.tags[i]) { - errors.metadata.tags[i] = 'Tag cannot be empty.'; - hasErrors = true; - } - } - - // Sub-tasks text cannot be empty - for (let i = 0; i < values.subTasks.length; i++) { - if (!values.subTasks[i].text) { - errors.subTasks[i] = { - text: 'Sub-task text cannot be empty.' - }; - hasErrors = true; - } - } - - // Comments text cannot be empty - for (let i = 0; i < values.comments.length; i++) { - if (!values.comments[i].text) { - errors.comments[i] = { - text: 'Comment text cannot be empty.' - }; - hasErrors = true; - } - } - - return hasErrors ? errors : {}; - }} - onSubmit={(values, { setSubmitting }) => { - handleSubmit(values); - setSubmitting(false); + initialValues={taskData} + validate={validate} + onSubmit={(values, { setSubmitting, resetForm }) => { + handleSubmit(values, setSubmitting, resetForm); }} > {({ + dirty, values, handleChange, isSubmitting }) => ( -
-
-
-
-