Task editing

This commit is contained in:
Gordon
2021-04-05 02:02:03 +01:00
parent 708b130059
commit 29fca87a99
13 changed files with 509 additions and 57 deletions

View File

@ -19,6 +19,7 @@ function App() {
const [dateFormat, setDateFormat] = useState('');
const [task, setTask] = useState({});
const [columnName, setColumnName] = useState('');
const [columnNames, setColumnNames] = useState([] as string[]);
window.addEventListener('message', event => {
switch (event.data.type) {
@ -39,6 +40,7 @@ function App() {
case 'task':
setTask(event.data.task);
setColumnName(event.data.columnName);
setColumnNames(Object.keys(event.data.index.columns));
break;
}
setType(event.data.type);
@ -46,7 +48,7 @@ function App() {
});
return (
<div>
<React.Fragment>
{
type === 'index' &&
<React.Fragment>
@ -68,11 +70,12 @@ function App() {
<TaskEditor
task={task as KanbnTask|null}
columnName={columnName}
columnNames={columnNames}
dateFormat={dateFormat}
vscode={vscode}
/>
}
</div>
</React.Fragment>
);
}

View File

@ -88,11 +88,11 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
<h2 className="kanbn-column-name">
{
startedColumns.indexOf(columnName) > -1 &&
<i className="kanbn-column-icon codicon codicon-chevron-right"></i>
<i className="codicon codicon-chevron-right"></i>
}
{
completedColumns.indexOf(columnName) > -1 &&
<i className="kanbn-column-icon codicon codicon-check"></i>
<i className="codicon codicon-check"></i>
}
{columnName}
<span className="kanbn-column-count">{column.length || ''}</span>
@ -101,7 +101,7 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
className="kanbn-create-task-button"
onClick={() => {
vscode.postMessage({
command: 'kanbn.create',
command: 'kanbn.addTask',
columnName
})
}}
@ -122,9 +122,9 @@ const Board = ({ columns, startedColumns, completedColumns, dateFormat, vscode }
snapshot.isDraggingOver ? 'drag-over' : null
].filter(i => i).join(' ')}
>
{column.map((task, index) => <TaskItem
{column.map((task, position) => <TaskItem
task={task}
index={index}
position={position}
dateFormat={dateFormat}
vscode={vscode}
/>)}

12
src/KanbnTask.d.ts vendored
View File

@ -7,11 +7,11 @@ declare type KanbnTask = {
remainingWorkload?: number,
progress?: number,
metadata: {
created: Date,
updated?: Date,
started?: Date,
due?: Date,
completed?: Date,
created: string,
updated?: string,
started?: string,
due?: string,
completed?: string,
assigned?: string,
tags?: string[]
},
@ -25,7 +25,7 @@ declare type KanbnTask = {
}>,
comments: Array<{
author: string,
date: Date,
date: string,
text: string
}>
};

View File

@ -1,18 +1,213 @@
import React from "react";
import React, { useReducer, useCallback } from "react";
import formatDate from 'dateformat';
import VSCodeApi from "./VSCodeApi";
import { paramCase } from 'param-case';
import produce from 'immer';
import { set, has } from 'lodash';
import * as gitUsername from 'git-user-name';
const TaskEditor = ({ task, columnName, dateFormat, vscode }: {
// https://levelup.gitconnected.com/handling-complex-form-state-using-react-hooks-76ee7bc937
function reducer(state, action) {
if (action.constructor === Function) {
return { ...state, ...action(state) };
}
if (action.constructor === Object) {
if (has(action, "_path") && has(action, "_value")) {
const { _path, _value } = action;
return produce(state, draft => {
set(draft, _path, _value);
});
} else {
return { ...state, ...action };
}
}
}
const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
task: KanbnTask|null,
columnName: string,
columnNames: string[],
dateFormat: string,
vscode: VSCodeApi
}) => {
const editing = task !== null;
const [taskData, setTaskData] = useReducer(reducer, {
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') : '',
assigned: (task && 'assigned' in task.metadata) ? task.metadata.assigned : gitUsername(),
tags: (task && 'tags' in task.metadata) ? task.metadata.tags : []
},
relations: [],
subTasks: [],
comments: []
});
const handleChange = useCallback(({ target: { value, name, type } }) => {
const updatePath = name.split(".");
// Handle updating checkbox states (depends on previous state)
if (type === 'checkbox') {
setTaskData((previousState) => ({
[name]: !previousState[name]
}));
return;
}
// Handle updating root-level properties
if (updatePath.length === 1) {
const [key] = updatePath;
const newTaskData = {
[key]: value
};
// If the name is updated, generate a new id and set the webview panel title
if (key === 'name') {
newTaskData['id'] = paramCase(value);
vscode.postMessage({
command: 'kanbn.updatePanelTitle',
title: value || 'Untitled task'
});
}
setTaskData(newTaskData);
}
// Handle updating nested properties using _path and _value
if (updatePath.length > 1) {
setTaskData({
_path: updatePath,
_value: value
});
}
}, []);
const handleSubmit = e => {
e.preventDefault();
// If a task prop was passed in, we're updating a task, otherwise we're creating a new task
if (editing) {
vscode.postMessage({
command: 'kanbn.update'
});
} else {
vscode.postMessage({
command: 'kanbn.create'
});
}
console.log(e);
};
return (
<div>
Viewing or editing task: {task ? task.name : '(creating new task)'}<br />
Column: {columnName}
</div>
<form className="kanbn-task-editor" onSubmit={handleSubmit}>
<h1 className="kanbn-task-editor-title">{editing ? 'Update task' : 'Create new task'}</h1>
<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">
<p>Name</p>
<input
className="kanbn-task-field-input"
placeholder="Name"
name="name"
value={taskData.name}
onChange={handleChange}
></input>
</label>
<span className="kanbn-task-id">{taskData.id}</span>
</div>
<div className="kanbn-task-field kanbn-task-field-description">
<label className="kanbn-task-field-label">
<p>Description</p>
<textarea
className="kanbn-task-field-textarea"
placeholder="Description"
name="description"
value={taskData.description}
onChange={handleChange}
></textarea>
</label>
</div>
</div>
<div className="kanbn-task-editor-column-right">
<div>
<button
type="submit"
className="kanbn-task-editor-button kanbn-task-editor-button-submit"
title="Save"
>
Save
</button>
<button
type="button"
className="kanbn-task-editor-button kanbn-task-editor-button-delete"
title="Delete"
>
Delete
</button>
</div>
<div className="kanbn-task-field kanbn-task-field-column">
<label className="kanbn-task-field-label">
<p>Column</p>
<select
className="kanbn-task-field-select"
name="column"
value={taskData.column}
onChange={handleChange}
>
{columnNames.map(c => <option value={c}>{c}</option>)}
</select>
</label>
</div>
<div className="kanbn-task-field kanbn-task-field-assigned">
<label className="kanbn-task-field-label">
<p>Assigned to</p>
<input
className="kanbn-task-field-input"
name="metadata.assigned"
value={taskData.metadata.assigned}
onChange={handleChange}
></input>
</label>
</div>
<div className="kanbn-task-field kanbn-task-field-due">
<label className="kanbn-task-field-label">
<p>Due date</p>
<input
type="date"
className="kanbn-task-field-input"
name="metadata.due"
value={taskData.metadata.due}
onChange={handleChange}
></input>
</label>
</div>
<div className="kanbn-task-field kanbn-task-field-progress">
<label className="kanbn-task-field-label">
<p>Progress</p>
<input
type="number"
className="kanbn-task-field-input"
name="progress"
value={taskData.progress}
onChange={handleChange}
min="0"
max="1"
step="0.05"
></input>
</label>
</div>
</div>
</div>
</form>
);
}

View File

@ -1,11 +1,12 @@
import React from "react";
import { Draggable } from "react-beautiful-dnd";
import formatDate from 'dateformat';
import { paramCase } from 'param-case';
import VSCodeApi from "./VSCodeApi";
const TaskItem = ({ task, index, dateFormat, vscode }: {
const TaskItem = ({ task, position, dateFormat, vscode }: {
task: KanbnTask,
index: number,
position: number,
dateFormat: string,
vscode: VSCodeApi
}) => {
@ -18,7 +19,7 @@ const TaskItem = ({ task, index, dateFormat, vscode }: {
<Draggable
key={task.id}
draggableId={task.id}
index={index}
index={position}
>
{(provided, snapshot) => {
return (
@ -59,7 +60,7 @@ const TaskItem = ({ task, index, dateFormat, vscode }: {
return (
<span className={[
'kanbn-task-tag',
`kanbn-task-tag-${tag}`
`kanbn-task-tag-${paramCase(tag)}`
].join(' ')}>
{tag}
</span>

View File

@ -23,7 +23,7 @@ body {
padding-left: 8px;
}
.kanbn-column-icon {
.kanbn-column-name .codicon {
font-size: 0.8em !important;
margin-right: 0.5em;
}
@ -65,7 +65,7 @@ body {
.kanbn-column-task-list {
margin: 0 8px;
border-left: 3px var(--vscode-activityBar-inactiveForeground) solid;
border-left: 4px var(--vscode-activityBar-inactiveForeground) solid;
}
.kanbn-column-task-list.drag-over {
@ -200,7 +200,67 @@ body {
position: absolute;
bottom: -2px;
left: 0;
height: 4px;
height: 6px;
background-color: #3c7;
opacity: 0.7;
}
.kanbn-task-editor-title {
font-size: 1.5em;
margin-top: 0;
padding-bottom: 0.5em;
border-bottom: 1px var(--vscode-activityBar-inactiveForeground) solid;
}
.kanbn-task-editor-column-left {
width: 70%;
padding-right: 1em;
}
.kanbn-task-editor-column-right {
width: 30%;
}
.kanbn-task-field {
margin-bottom: 1em;
}
.kanbn-task-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-field-input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.kanbn-task-field-input,
.kanbn-task-field-select,
.kanbn-task-field-textarea {
box-sizing: border-box;
display: block;
width: 100%;
padding: 8px;
margin: 8px 0;
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);
border: 1px transparent solid;
}
.kanbn-task-field-textarea {
min-height: 200px;
resize: vertical;
}
.kanbn-task-field-input:hover, .kanbn-task-field-input:focus {
border-color: var(--vscode-input-border);
}
.kanbn-task-id {
font-style: italic;
opacity: 0.8;
}