Edit relations and sub-tasks

This commit is contained in:
Gordon 2021-04-05 23:09:43 +01:00
parent be9daf1e42
commit 6efe282b81
9 changed files with 379 additions and 120 deletions

View File

@ -42,12 +42,19 @@ export default class KanbnBoardPanel {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return;
}
let tasks: any[];
try {
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',
index,
tasks: (await KanbnBoardPanel.currentPanel._kanbn.loadAllTrackedTasks(index)).map(
task => KanbnBoardPanel.currentPanel!._kanbn.hydrateTask(index, task)
),
tasks,
startedColumns: index.options.startedColumns ?? [],
completedColumns: index.options.completedColumns ?? [],
dateFormat: KanbnBoardPanel.currentPanel._kanbn.getDateFormat(index)

View File

@ -1,5 +1,6 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { v4 as uuidv4 } from 'uuid';
export default class KanbnTaskPanel {
private static readonly viewType = 'react';
@ -28,14 +29,21 @@ export default class KanbnTaskPanel {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return;
}
let task: any = null;
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) {
try {
task = await kanbn.hydrateTask(index, await kanbn.getTask(taskId));
} catch (error) {
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
return;
}
task = tasks.find(t => t.id === taskId) ?? null;
}
// If no columnName is specified, use the first column
@ -60,7 +68,7 @@ export default class KanbnTaskPanel {
index,
task,
columnName: taskPanel._columnName,
tasks: await kanbn.loadAllTrackedTasks(index),
tasks,
dateFormat: kanbn.getDateFormat(index)
});
}

41
package-lock.json generated
View File

@ -3355,6 +3355,12 @@
"resolved": "https://registry.npmjs.org/@types/dateformat/-/dateformat-3.0.1.tgz",
"integrity": "sha512-KlPPdikagvL6ELjWsljbyDIPzNCeliYkqRpI+zea99vBBbCIA5JNshZAwQKTON139c87y9qvTFVgkFd14rtS4g=="
},
"@types/git-user-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/git-user-name/-/git-user-name-2.0.0.tgz",
"integrity": "sha512-bZhPykkyyPdHW2wMc30aLIWntMIMO49jWKGQipPg3RQNGq2LdAnW7OCinHew+M8EpZ5qHNyFynIiou+UyxOfww==",
"dev": true
},
"@types/jest": {
"version": "23.3.14",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.14.tgz",
@ -3417,6 +3423,12 @@
"integrity": "sha512-42zEJkBpNfMEAvWR5WlwtTH22oDzcMjFsL9gDGExwF8X8WvAiw7Vwop7hPw03QT8TKfec83LwbHj6SvpqM4ELQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
"dev": true
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@ -17478,6 +17490,14 @@
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"request-promise-core": {
@ -18859,6 +18879,12 @@
"requires": {
"websocket-driver": ">=0.5.1"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
@ -20076,10 +20102,9 @@
"dev": true
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"validate-npm-package-license": {
"version": "3.0.4",
@ -20913,6 +20938,14 @@
"requires": {
"ansi-colors": "^3.0.0",
"uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
}
},
"webpack-manifest-plugin": {

View File

@ -39,6 +39,7 @@
"react-beautiful-dnd": "12.2.0",
"react-dom": "^16.3.2",
"terser": "3.14.1",
"uuid": "^8.3.2",
"vscode": "^1.1.37",
"vscode-codicons": "^0.0.15"
},
@ -51,11 +52,13 @@
"eject": "react-scripts eject"
},
"devDependencies": {
"@types/git-user-name": "^2.0.0",
"@types/jest": "^23.3.14",
"@types/lodash": "^4.14.168",
"@types/node": "^10.17.56",
"@types/react": "^16.14.5",
"@types/react-dom": "^16.9.12",
"@types/uuid": "^8.3.0",
"react-scripts": "^2.1.8",
"rewire": "^4.0.1",
"typescript": "^4.0.2"

View File

@ -18,13 +18,14 @@ function App() {
const [completedColumns, setCompletedColumns] = useState([]);
const [dateFormat, setDateFormat] = useState('');
const [task, setTask] = useState({});
const [tasks, setTasks] = useState({});
const [columnName, setColumnName] = useState('');
const [columnNames, setColumnNames] = useState([] as string[]);
window.addEventListener('message', event => {
const tasks = Object.fromEntries(event.data.tasks.map(task => [task.id, task]));
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(
@ -39,6 +40,7 @@ function App() {
case 'task':
setTask(event.data.task);
setTasks(tasks);
setColumnName(event.data.columnName);
setColumnNames(Object.keys(event.data.index.columns));
break;
@ -69,6 +71,7 @@ function App() {
type === 'task' &&
<TaskEditor
task={task as KanbnTask|null}
tasks={tasks}
columnName={columnName}
columnNames={columnNames}
dateFormat={dateFormat}

View File

@ -64,12 +64,7 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
}) => {
const [, setColumns] = useState(columns);
return (
<div
className="kanbn-board"
style={{
display: "flex"
}}
>
<div className="kanbn-board">
<DragDropContext
onDragEnd={result => onDragEnd(result, columns, setColumns, vscode)}
>
@ -80,9 +75,6 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
'kanbn-column',
`kanbn-column-${paramCase(columnName)}`
].join(' ')}
style={{
flex: 1
}}
key={columnName}
>
<h2 className="kanbn-column-name">

1
src/KanbnTask.d.ts vendored
View File

@ -1,4 +1,5 @@
declare type KanbnTask = {
uuid?: string,
id: string,
name: string,
description: string,

View File

@ -1,12 +1,13 @@
import React from 'react';
import { Formik } from 'formik';
import { Formik, Form, Field, ErrorMessage, FieldArray } from 'formik';
import formatDate from 'dateformat';
import VSCodeApi from './VSCodeApi';
import { paramCase } from 'param-case';
import * as gitUsername from 'git-user-name';
import gitUsername from 'git-user-name';
const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, vscode }: {
task: KanbnTask|null,
tasks: Record<string, KanbnTask>,
columnName: string,
columnNames: string[],
dateFormat: string,
@ -32,6 +33,7 @@ const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
<h1 className="kanbn-task-editor-title">{editing ? 'Update task' : 'Create new task'}</h1>
<Formik
initialValues={{
uuid: task ? task.uuid : '',
id: task ? task.id : '',
name: task ? task.name : '',
description: task ? task.description : '',
@ -39,22 +41,23 @@ const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
progress: task ? task.progress : 0,
metadata: {
due: (task && 'due' in task.metadata) ? formatDate(new Date(task.metadata.due!), 'yyyy-mm-dd') : '',
assigned: (task && 'assigned' in task.metadata) ? task.metadata.assigned : gitUsername(),
assigned: (task && 'assigned' in task.metadata) ? task.metadata.assigned : (gitUsername() || ''),
tags: (task && 'tags' in task.metadata) ? task.metadata.tags : []
},
relations: [],
subTasks: [],
comments: []
relations: task ? task.relations : [],
subTasks: task ? task.subTasks : [],
comments: task ? task.comments : []
}}
validate={values => {
const errors: { name?: string } = {};
// if (!values.email) {
// errors.email = 'Required';
// } else if (
// !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
// ) {
// errors.email = 'Invalid email address';
// }
if (!values.name) {
errors.name = 'Task name is required.';
}
// 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.';
}
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
@ -73,125 +76,246 @@ const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
>
{({
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting
}) => (
<form onSubmit={handleSubmit}>
<div
style={{
display: "flex"
}}
>
<Form>
<div style={{ display: "flex" }}>
<div className="kanbn-task-editor-column-left">
<div className="kanbn-task-field kanbn-task-field-name">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-name">
<label className="kanbn-task-editor-field-label">
<p>Name</p>
<input
className="kanbn-task-field-input"
placeholder="Name"
<Field
className="kanbn-task-editor-field-input"
name="name"
value={values.name}
placeholder="Name"
onChange={e => {
handleChange(e);
handleUpdateName(e, values);
}}
onBlur={handleBlur}
></input>
/>
</label>
{errors.name && touched.name && errors.name}
<span className="kanbn-task-id">{values.id}</span>
<div className="kanbn-task-editor-id">{values.id}</div>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="name"
/>
</div>
<div className="kanbn-task-field kanbn-task-field-description">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-description">
<label className="kanbn-task-editor-field-label">
<p>Description</p>
<textarea
className="kanbn-task-field-textarea"
placeholder="Description"
<Field
className="kanbn-task-editor-field-textarea"
as="textarea"
name="description"
value={values.description}
onChange={handleChange}
onBlur={handleBlur}
></textarea>
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="description"
/>
</div>
<div className="kanbn-task-editor-field kanbn-task-editor-field-relations">
<h2 className="kanbn-task-editor-title">Relations</h2>
<FieldArray name="relations">
{({ insert, remove, push }) => (
<div>
{values.relations.length > 0 && values.relations.map((relation, index) => (
<div className="kanbn-task-editor-row kanbn-task-editor-row-relation" key={index}>
<div className="kanbn-task-editor-column kanbn-task-editor-field-relation-type">
<Field
className="kanbn-task-editor-field-input"
name={`relations.${index}.type`}
placeholder="Relation type"
/>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`relations.${index}.type`}
/>
</div>
<div className="kanbn-task-editor-column kanbn-task-editor-field-relation-task">
<Field
className="kanbn-task-editor-field-select"
as="select"
name={`relations.${index}.task`}
>
{Object.keys(tasks).map(t => <option value={t}>{t}</option>)}
</Field>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name={`relations.${index}.task`}
/>
</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"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>Delete
</button>
</div>
</div>
))}
<div className="kanbn-task-editor-buttons">
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-add"
onClick={() => push({ type: '', task: '' })}
>
<i className="codicon codicon-plus"></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">
{({ 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"
onClick={() => remove(index)}
>
<i className="codicon codicon-trash"></i>Delete
</button>
</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: '' })}
>
<i className="codicon codicon-plus"></i>Add sub-task
</button>
</div>
</div>
)}
</FieldArray>
</div>
</div>
<div className="kanbn-task-editor-column-right">
<div>
<div className="kanbn-task-editor-buttons">
{editing && <button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Delete"
>
<i className="codicon codicon-trash"></i>Delete
</button>}
<button
type="submit"
className="kanbn-task-editor-button kanbn-task-editor-button-submit"
title="Save"
disabled={isSubmitting}
>Save</button>
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Delete"
>Delete</button>
>
<i className="codicon codicon-save"></i>Save
</button>
</div>
<div className="kanbn-task-field kanbn-task-field-column">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-column">
<label className="kanbn-task-editor-field-label">
<p>Column</p>
<select
className="kanbn-task-field-select"
<Field
className="kanbn-task-editor-field-select"
as="select"
name="column"
value={values.column}
onChange={handleChange}
onBlur={handleBlur}
>
{columnNames.map(c => <option value={c}>{c}</option>)}
</select>
</Field>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="column"
/>
</div>
<div className="kanbn-task-field kanbn-task-field-assigned">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-assigned">
<label className="kanbn-task-editor-field-label">
<p>Assigned to</p>
<input
className="kanbn-task-field-input"
placeholder="Assigned to"
<Field
className="kanbn-task-editor-field-input"
name="metadata.assigned"
value={values.metadata.assigned}
onChange={handleChange}
onBlur={handleBlur}
></input>
placeholder="Assigned to"
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="metadata.assigned"
/>
</div>
<div className="kanbn-task-field kanbn-task-field-due">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-due">
<label className="kanbn-task-editor-field-label">
<p>Due date</p>
<input
<Field
className="kanbn-task-editor-field-input"
type="date"
className="kanbn-task-field-input"
name="metadata.due"
value={values.metadata.due}
onChange={handleChange}
onBlur={handleBlur}
></input>
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="metadata.due"
/>
</div>
<div className="kanbn-task-field kanbn-task-field-progress">
<label className="kanbn-task-field-label">
<div className="kanbn-task-editor-field kanbn-task-editor-field-progress">
<label className="kanbn-task-editor-field-label">
<p>Progress</p>
<input
<Field
className="kanbn-task-editor-field-input"
type="number"
className="kanbn-task-field-input"
name="progress"
value={values.progress}
onChange={handleChange}
onBlur={handleBlur}
min="0"
max="1"
step="0.05"
></input>
/>
</label>
<ErrorMessage
className="kanbn-task-editor-field-errors"
component="div"
name="progress"
/>
</div>
</div>
</div>
</form>
</Form>
)}
</Formik>
</div>

View File

@ -13,6 +13,14 @@ body {
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
}
.kanbn-board {
display: flex;
}
.kanbn-column {
flex: 1;
}
.kanbn-column-name {
color: var(--vscode-editor-foreground);
font-size: 0.8em;
@ -152,7 +160,7 @@ body {
position: relative;
top: 1px;
font-size: 0.9em !important;
margin-right: 4px;
margin-right: 0.5em;
}
.kanbn-task-tag {
@ -167,7 +175,7 @@ body {
}
.kanbn-task-tag-Nothing {
background-color: #aab;
background-color: #6bf;
color: #333;
}
@ -212,6 +220,12 @@ body {
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
}
.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-column-left {
width: 70%;
padding-right: 1em;
@ -221,11 +235,11 @@ body {
width: 30%;
}
.kanbn-task-field {
.kanbn-task-editor-field {
margin-bottom: 1em;
}
.kanbn-task-field-label p {
.kanbn-task-editor-field-label p {
color: var(--vscode-editor-foreground);
font-size: 0.8em;
letter-spacing: 0.1em;
@ -234,13 +248,14 @@ body {
padding: 4px 0;
}
body.vscode-dark .kanbn-task-field-input[type="date"]::-webkit-calendar-picker-indicator {
body.vscode-dark .kanbn-task-editor-field-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.kanbn-task-field-input,
.kanbn-task-field-select,
.kanbn-task-field-textarea {
.kanbn-task-editor-field-input,
.kanbn-task-editor-field-select,
.kanbn-task-editor-field-checkbox,
.kanbn-task-editor-field-textarea {
box-sizing: border-box;
display: block;
width: 100%;
@ -251,16 +266,89 @@ body.vscode-dark .kanbn-task-field-input[type="date"]::-webkit-calendar-picker-i
border: 1px transparent solid;
}
.kanbn-task-field-textarea {
.kanbn-task-editor-field-select {
padding-bottom: 7px;
}
.kanbn-task-editor-field-textarea {
min-height: 200px;
resize: vertical;
}
.kanbn-task-field-input:hover, .kanbn-task-field-input:focus {
.kanbn-task-editor-field-input:hover, .kanbn-task-editor-field-input:focus {
border-color: var(--vscode-input-border);
}
.kanbn-task-id {
.kanbn-task-editor-buttons {
text-align: right;
}
.kanbn-task-editor-button {
outline: none;
border: none;
background-color: var(--vscode-button-background);
color: var(--vscode-button-foreground);
padding: 9px 1em;
margin-left: 8px;
}
.kanbn-task-editor-button .codicon {
font-size: 11px !important;
margin-right: 0.5em;
position: relative;
top: 1px;
}
.kanbn-task-editor-button:hover, .kanbn-task-editor-button:focus {
background-color: var(--vscode-button-hoverBackground);
}
.kanbn-task-editor-button-delete:hover, .kanbn-task-editor-button-delete:focus {
background-color: #f42;
}
.kanbn-task-editor-field-errors {
font-weight: bold;
color: #f42;
}
.kanbn-task-editor-id {
margin-bottom: 8px;
font-style: italic;
opacity: 0.8;
}
.kanbn-task-editor-row {
display: flex;
}
.kanbn-task-editor-column {
flex: 1;
margin-right: 8px;
}
.kanbn-task-editor-column:last-child {
margin-right: 0;
}
.kanbn-task-editor-field-relation-task {
flex: 2;
}
.kanbn-task-editor-field-subtask-completed {
flex: 0 0 2em;
padding: 10px 0;
}
.kanbn-task-editor-column-buttons {
white-space: nowrap;
flex: 0;
}
.kanbn-task-editor-field .kanbn-task-editor-buttons {
margin-top: 8px;
}
.kanbn-task-editor-column-buttons .kanbn-task-editor-button {
margin: 8px 0;
}