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

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