This commit is contained in:
Gordon 2021-04-07 21:25:57 +01:00
parent 63bf065331
commit a26d1ba7c0
6 changed files with 594 additions and 500 deletions

View File

@ -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<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 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'];

View File

@ -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.

View File

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

View File

@ -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<string, KanbnTask>,
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,39 +104,8 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
return false;
};
return (
<div className="kanbn-task-editor">
<h1 className="kanbn-task-editor-title">
{editing ? 'Update task' : 'Create new task'}
{editing && <span className="kanbn-task-editor-dates">
{
[
'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(', ')
}
</span>}
</h1>
<Formik
initialValues={{
uuid: task ? (task.uuid || '') : '',
id: task ? task.id : '',
name: task ? task.name : '',
description: task ? task.description : '',
column: columnName,
progress: task ? task.progress : 0,
metadata: {
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 : []
}}
validate={(values: KanbnTaskValidationInput): KanbnTaskValidationOutput|{} => {
// Validate form data
const validate = (values: KanbnTaskValidationInput): KanbnTaskValidationOutput|{} => {
let hasErrors = false;
const errors: KanbnTaskValidationOutput = {
name: '',
@ -128,7 +123,7 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
}
// Check if the id is already in use
if (values.id in tasks && tasks[values.id].uuid !== values.uuid) {
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;
}
@ -162,17 +157,36 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
}
return hasErrors ? errors : {};
}}
onSubmit={(values, { setSubmitting }) => {
handleSubmit(values);
setSubmitting(false);
};
return (
<div className="kanbn-task-editor">
<Formik
initialValues={taskData}
validate={validate}
onSubmit={(values, { setSubmitting, resetForm }) => {
handleSubmit(values, setSubmitting, resetForm);
}}
>
{({
dirty,
values,
handleChange,
isSubmitting
}) => (
<React.Fragment>
<h1 className="kanbn-task-editor-title">
{editing ? 'Update task' : 'Create new task'}
{dirty && <span className="kanbn-task-editor-dirty">*</span>}
{editing && <span className="kanbn-task-editor-dates">
{
[
'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(', ')
}
</span>}
</h1>
<Form>
<div className="kanbn-task-editor-form">
<div className="kanbn-task-editor-column-left">
@ -573,6 +587,7 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
</div>
</div>
</Form>
</React.Fragment>
)}
</Formik>
</div>

View File

@ -64,6 +64,7 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
<div className="kanbn-task-row">
{
'tags' in task.metadata &&
task.metadata.tags!.length > 0 &&
<div className="kanbn-task-data kanbn-task-tags">
{task.metadata.tags!.map(tag => {
return (
@ -81,6 +82,7 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
<div className="kanbn-task-row">
{
'assigned' in task.metadata &&
!!task.metadata.assigned &&
<div className="kanbn-task-data kanbn-task-assigned">
<i className="codicon codicon-account"></i>{task.metadata.assigned}
</div>

View File

@ -120,6 +120,7 @@ body {
.kanbn-column-task-list {
margin: 0 8px;
border-left: 4px var(--vscode-activityBar-inactiveForeground) solid;
min-height: 46px;
}
.kanbn-column-task-list.drag-over {
@ -270,6 +271,11 @@ body {
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
}
.kanbn-task-editor-dirty {
margin-left: 8px;
color: #f42;
}
.kanbn-task-editor-dates {
font-size: var(--vscode-font-size);
font-style: italic;