Comments editor

This commit is contained in:
Gordon 2021-04-06 22:00:56 +01:00
parent d889148e56
commit 7ea93c6290
7 changed files with 335 additions and 81 deletions

View File

@ -7,5 +7,6 @@
"out": true // set this to false to include "out" folder in search results
},
"typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
"typescript.tsc.autoDetect": "off" // Turn off tsc task auto detection since we have the necessary task as npm scripts
"typescript.tsc.autoDetect": "off",
"todo-tree.tree.scanMode": "workspace only" // Turn off tsc task auto detection since we have the necessary task as npm scripts
}

View File

@ -102,6 +102,10 @@ export default class KanbnTaskPanel {
vscode.Uri.file(path.join(this._extensionPath, 'node_modules', 'vscode-codicons', 'dist'))
]
});
this._panel.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'))
};
// Set the webview's title to the kanbn task name
if (this._taskId !== null) {

View File

@ -91,13 +91,13 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
<button
type="button"
className="kanbn-create-task-button"
title={`Create task in ${columnName}`}
onClick={() => {
vscode.postMessage({
command: 'kanbn.addTask',
columnName
})
}}
title={`Create task in ${columnName}`}
>
<i className="codicon codicon-add"></i>
</button>

3
src/KanbnTask.d.ts vendored
View File

@ -1,3 +1,4 @@
// Note that Date properties will be converted to strings (ISO) when a task is serialized and passed as a prop
declare type KanbnTask = {
uuid?: string,
id: string,
@ -8,7 +9,7 @@ declare type KanbnTask = {
remainingWorkload?: number,
progress?: number,
metadata: {
created: string,
created?: string,
updated?: string,
started?: string,
due?: string,

View File

@ -5,6 +5,24 @@ import VSCodeApi from './VSCodeApi';
import { paramCase } from 'param-case';
import gitUsername from 'git-user-name';
interface KanbnTaskValidationOutput {
name: string,
metadata: {
tags: string[]
},
subTasks: Array<{
text: string
}>,
comments: Array<{
text: string
}>
}
interface KanbnTaskValidationInput extends KanbnTaskValidationOutput {
uuid: string,
id: string
}
const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }: {
task: KanbnTask|null,
tasks: Record<string, KanbnTask>,
@ -36,35 +54,55 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
});
};
// TODO progress bar below progress input
// TODO auto-colour tags while typing
// TODO comments
// TODO make sure all buttons have title attributes, maybe remove labels from array delete buttons?
// Check if a task's due date is in the past
const checkOverdue = (values: { metadata: { due?: string } }) => {
if ('due' in values.metadata && values.metadata.due !== undefined) {
return Date.parse(values.metadata.due) < (new Date()).getTime();
}
return false;
};
return (
<div className="kanbn-task-editor">
<h1 className="kanbn-task-editor-title">{editing ? 'Update task' : 'Create new task'}</h1>
<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 : '',
uuid: task ? (task.uuid || '') : '',
id: task ? task.id : '',
name: task ? task.name : '',
description: task ? task.description : '',
column: columnName,
progress: task ? task.progress : 0,
metadata: {
due: (task && 'due' in task.metadata) ? formatDate(new Date(task.metadata.due!), 'yyyy-mm-dd') : '',
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 : []
tags: (task && 'tags' in task.metadata) ? (task.metadata.tags || []) : []
},
relations: task ? task.relations : [],
subTasks: task ? task.subTasks : [],
comments: task ? task.comments : []
}}
validate={values => {
const errors: { name?: string } = {};
// TODO validation
validate={(values: KanbnTaskValidationInput): KanbnTaskValidationOutput => {
const errors: KanbnTaskValidationOutput = {
name: '',
metadata: {
tags: []
},
subTasks: [],
comments: []
};
// Task name cannot be empty
if (!values.name) {
@ -75,6 +113,32 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
if (values.id in tasks && tasks[values.id].uuid !== values.uuid) {
errors.name = 'There is already a task with the same name or id.';
}
// 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.';
}
}
// 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.'
};
}
}
// 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.'
};
}
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
@ -134,6 +198,63 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
name="description"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-subtasks">
<h2 className="kanbn-task-editor-title">Sub-tasks</h2>
<FieldArray name="subTasks">
{({ insert, remove, push }) => (
<div>
{values.subTasks.length > 0 && values.subTasks.map((subTask, index) => (
<div className="kanbn-task-editor-row kanbn-task-editor-row-subtask" key={index}>
<div className="kanbn-task-editor-column kanbn-task-editor-field-subtask-completed">
<Field
className="kanbn-task-editor-field-checkbox"
type="checkbox"
name={`subTasks.${index}.completed`}
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`subTasks.${index}.completed`}
/>
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-field-subtask-text">
<Field
className="kanbn-task-editor-field-input"
name={`subTasks.${index}.text`}
placeholder="Sub-task text"
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`subTasks.${index}.text`}
/>
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-column-buttons">
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Remove sub-task"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>
</button>
</div>
</div>
))}
<div className="kanbn-task-editor-buttons">
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-add"
title="Add sub-task"
onClick={() => push({ completed: false, text: '' })}
>
<i className="codicon codicon-tasklist"></i>Add sub-task
</button>
</div>
</div>
)}
</FieldArray>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-relations">
<h2 className="kanbn-task-editor-title">Relations</h2>
<FieldArray name="relations">
@ -171,9 +292,10 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Remove relation"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>Delete
<i className="codicon codicon-trash"></i>
</button>
</div>
</div>
@ -182,64 +304,74 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-add"
title="Add relation"
onClick={() => push({ type: '', task: '' })}
>
<i className="codicon codicon-plus"></i>Add relation
<i className="codicon codicon-link"></i>Add relation
</button>
</div>
</div>
)}
</FieldArray>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-subtasks">
<h2 className="kanbn-task-editor-title">Sub-tasks</h2>
<FieldArray name="subTasks">
<div className="kanbn-task-editor-field kanbn-task-editor-field-comments">
<h2 className="kanbn-task-editor-title">Comments</h2>
<FieldArray name="comments">
{({ insert, remove, push }) => (
<div>
{values.subTasks.length > 0 && values.subTasks.map((subTask, index) => (
<div className="kanbn-task-editor-row kanbn-task-editor-row-subtask" key={index}>
<div className="kanbn-task-editor-column kanbn-task-editor-field-subtask-completed">
<Field
className="kanbn-task-editor-field-checkbox"
type="checkbox"
name={`subTasks.${index}.completed`}
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`subTasks.${index}.completed`}
/>
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-field-subtask-text">
{values.comments.length > 0 && values.comments.map((comment, index) => (
<div className="kanbn-task-editor-row-comment" key={index}>
<div className="kanbn-task-editor-row">
<div className="kanbn-task-editor-column kanbn-task-editor-field-comment-author">
<Field
className="kanbn-task-editor-field-input"
name={`subTasks.${index}.text`}
placeholder="Sub-task text"
name={`comments.${index}.author`}
placeholder="Comment author"
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`subTasks.${index}.text`}
name={`comments.${index}.author`}
/>
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-field-comment-date">
{formatDate(comment.date, dateFormat)}
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-column-buttons">
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Remove comment"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>Delete
<i className="codicon codicon-trash"></i>
</button>
</div>
</div>
<div className="kanbn-task-editor-row">
<div className="kanbn-task-editor-column kanbn-task-editor-field-comment-text">
<Field
className="kanbn-task-editor-field-textarea"
as="textarea"
name={`comments.${index}.text`}
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`comments.${index}.text`}
/>
</div>
</div>
</div>
))}
<div className="kanbn-task-editor-buttons">
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-add"
onClick={() => push({ completed: false, text: '' })}
title="Add comment"
onClick={() => push({ text: '', date: new Date(), author: gitUsername() || '' })}
>
<i className="codicon codicon-plus"></i>Add sub-task
<i className="codicon codicon-comment"></i>Add comment
</button>
</div>
</div>
@ -252,7 +384,7 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
{editing && <button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Delete"
title="Delete task"
onClick={handleRemoveTask}
>
<i className="codicon codicon-trash"></i>Delete
@ -260,7 +392,7 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<button
type="submit"
className="kanbn-task-editor-button kanbn-task-editor-button-submit"
title="Save"
title="Save task"
disabled={isSubmitting}
>
<i className="codicon codicon-save"></i>Save
@ -298,11 +430,29 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
name="metadata.assigned"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-started">
<label className="kanbn-task-editor-field-label">
<p>Started date</p>
<Field
className="kanbn-task-editor-field-input"
type="date"
name="metadata.started"
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="metadata.started"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-due">
<label className="kanbn-task-editor-field-label">
<p>Due date</p>
<Field
className="kanbn-task-editor-field-input"
className={[
'kanbn-task-editor-field-input',
checkOverdue(values) ? 'kanbn-task-overdue' : null
].filter(i => i).join(' ')}
type="date"
name="metadata.due"
/>
@ -313,6 +463,21 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
name="metadata.due"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-completed">
<label className="kanbn-task-editor-field-label">
<p>Completed date</p>
<Field
className="kanbn-task-editor-field-input"
type="date"
name="metadata.completed"
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="metadata.completed"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-progress">
<label className="kanbn-task-editor-field-label">
<p>Progress</p>
@ -324,6 +489,9 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
max="1"
step="0.05"
/>
<div className="kanbn-task-progress" style={{
width: `${Math.min(1, Math.max(0, values.progress || 0)) * 100}%`
}}></div>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
@ -347,7 +515,14 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<Field
className="kanbn-task-editor-field-input"
name={`metadata.tags.${index}`}
placeholder="Tag name"
/>
<div
className={[
'kanbn-task-editor-tag-highlight',
`kanbn-task-tag-${paramCase(values.metadata.tags![index])}`
].join(' ')}
></div>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
@ -358,9 +533,10 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Remove tag"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>Delete
<i className="codicon codicon-trash"></i>
</button>
</div>
</div>
@ -369,9 +545,10 @@ const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-add"
title="Add tag"
onClick={() => push('')}
>
<i className="codicon codicon-plus"></i>Add tag
<i className="codicon codicon-tag"></i>Add tag
</button>
</div>
</div>

View File

@ -10,11 +10,20 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
dateFormat: string,
vscode: VSCodeApi
}) => {
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) : '';
const createdDate = 'created' in task.metadata ? formatDate(task.metadata.created, dateFormat) : null;
const updatedDate = 'updated' in task.metadata ? formatDate(task.metadata.updated, dateFormat) : null;
const startedDate = 'started' in task.metadata ? formatDate(task.metadata.started, dateFormat) : null;
const dueDate = 'due' in task.metadata ? formatDate(task.metadata.due, dateFormat) : null;
const completedDate = 'completed' in task.metadata ? formatDate(task.metadata.completed, dateFormat) : null;
// Check if a task's due date is in the past
const checkOverdue = (task: KanbnTask) => {
if ('due' in task.metadata && task.metadata.due !== undefined) {
return Date.parse(task.metadata.due) < (new Date()).getTime();
}
return false;
};
return (
<Draggable
key={task.id}
@ -85,7 +94,12 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
dueDate ? `Due ${dueDate}` : null,
completedDate ? `Completed ${completedDate}` : null
].filter(i => i).join('\n')}>
<i className="codicon codicon-clock"></i>{updatedDate || createdDate}
<i
className={[
'codicon codicon-clock',
checkOverdue(task) ? 'kanbn-task-overdue' : null
].filter(i => i).join(' ')}
></i>{updatedDate || createdDate}
</div>
}
{
@ -111,7 +125,7 @@ const TaskItem = ({ task, position, dateFormat, vscode }: {
task.workload !== undefined &&
task.progress !== undefined &&
<div className="kanbn-task-progress" style={{
width: `${task.progress * 100}%`
width: `${Math.min(1, Math.max(0, task.progress)) * 100}%`
}}></div>
}
</div>

View File

@ -1,7 +1,8 @@
body {
margin: 0;
padding: 1em;
font-family: sans-serif;
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
background-color: var(--vscode-editor-background);
color: var(--vscode-foreground);
}
@ -204,6 +205,10 @@ body {
color: #333;
}
.kanbn-task-overdue {
color: #f42 !important;
}
.kanbn-task-progress {
position: absolute;
bottom: -2px;
@ -220,14 +225,27 @@ body {
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
}
.kanbn-task-editor-dates {
font-size: var(--vscode-font-size);
font-style: italic;
font-weight: normal;
opacity: 0.8;
float: right;
}
.kanbn-task-editor-form {
display: flex;
}
.kanbn-task-editor-field .kanbn-task-editor-title {
font-size: 1.1em;
padding: 0.5em 0;
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
.kanbn-task-editor-field .kanbn-task-editor-title,
.kanbn-task-editor-field-label p {
color: var(--vscode-editor-foreground);
font-size: 0.8em;
letter-spacing: 0.1em;
font-weight: bold;
text-transform: uppercase;
padding: 4px 0;
border-bottom: none;
}
.kanbn-task-editor-column-left {
@ -243,15 +261,6 @@ body {
margin-bottom: 1em;
}
.kanbn-task-editor-field-label p {
color: var(--vscode-editor-foreground);
font-size: 0.8em;
letter-spacing: 0.1em;
font-weight: bold;
text-transform: uppercase;
padding: 4px 0;
}
body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
@ -270,6 +279,11 @@ body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-p
border: 1px transparent solid;
}
.kanbn-task-editor-field-input[type=date] {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
}
.kanbn-task-editor-field-select {
padding-bottom: 7px;
}
@ -356,3 +370,46 @@ body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-p
.kanbn-task-editor-column-buttons .kanbn-task-editor-button {
margin: 8px 0;
}
.kanbn-task-editor-field-progress {
position: relative;
}
.kanbn-task-editor-field-progress .kanbn-task-progress {
bottom: 0;
height: 4px;
opacity: 1;
}
.kanbn-task-editor-column .kanbn-task-editor-button-delete .codicon {
margin-right: 0;
}
.kanbn-task-editor-field-tag {
position: relative;
}
.kanbn-task-editor-tag-highlight {
position: absolute;
bottom: 8px;
left: 0;
height: 4px;
width: 100%;
}
.kanbn-task-editor-row-comment {
padding-bottom: 1em;
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
margin-bottom: 1em;
}
.kanbn-task-editor-field-comment-date {
padding: 16px 0;
text-align: right;
font-style: italic;
opacity: 0.8;
}
.kanbn-task-editor-field-comment-text .kanbn-task-editor-field-textarea {
min-height: 90px;
}