Refactor task editor to use formik

This commit is contained in:
Gordon
2021-04-05 13:43:59 +01:00
parent 29fca87a99
commit be9daf1e42
4 changed files with 213 additions and 199 deletions

View File

@ -38,4 +38,4 @@ The kanbn board has a default style which is based on the current vscode theme,
### Task editor ### Task editor
- `// TODO` - `// TODO add task editor classes`

42
package-lock.json generated
View File

@ -6738,6 +6738,11 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true "dev": true
}, },
"deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
},
"default-gateway": { "default-gateway": {
"version": "2.7.2", "version": "2.7.2",
"resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz",
@ -8968,6 +8973,20 @@
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
} }
}, },
"formik": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz",
"integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==",
"requires": {
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.14",
"lodash-es": "^4.17.14",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^1.10.0"
}
},
"forwarded": { "forwarded": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@ -10700,11 +10719,6 @@
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
"dev": true "dev": true
}, },
"immer": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.1.tgz",
"integrity": "sha512-7CCw1DSgr8kKYXTYOI1qMM/f5qxT5vIVMeGLDCDX8CSxsggr1Sjdoha4OhsP0AZ1UvWbyZlILHvLjaynuu02Mg=="
},
"import-cwd": { "import-cwd": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -12244,6 +12258,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash._reinterpolate": { "lodash._reinterpolate": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@ -16792,6 +16811,11 @@
"integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q==", "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q==",
"dev": true "dev": true
}, },
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -19588,6 +19612,11 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
}, },
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -19712,8 +19741,7 @@
"tslib": { "tslib": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
"dev": true
}, },
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",

View File

@ -32,9 +32,8 @@
"@basementuniverse/kanbn": "file:~/Projects/kanbn", "@basementuniverse/kanbn": "file:~/Projects/kanbn",
"@types/dateformat": "^3.0.1", "@types/dateformat": "^3.0.1",
"dateformat": "^4.5.1", "dateformat": "^4.5.1",
"formik": "^2.2.6",
"git-user-name": "^2.0.0", "git-user-name": "^2.0.0",
"immer": "^9.0.1",
"lodash": "^4.17.21",
"param-case": "^3.0.4", "param-case": "^3.0.4",
"react": "^16.3.2", "react": "^16.3.2",
"react-beautiful-dnd": "12.2.0", "react-beautiful-dnd": "12.2.0",

View File

@ -1,29 +1,10 @@
import React, { useReducer, useCallback } from "react"; import React from 'react';
import { Formik } from 'formik';
import formatDate from 'dateformat'; import formatDate from 'dateformat';
import VSCodeApi from "./VSCodeApi"; import VSCodeApi from './VSCodeApi';
import { paramCase } from 'param-case'; import { paramCase } from 'param-case';
import produce from 'immer';
import { set, has } from 'lodash';
import * as gitUsername from 'git-user-name'; import * as gitUsername from 'git-user-name';
// 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 }: { const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
task: KanbnTask|null, task: KanbnTask|null,
columnName: string, columnName: string,
@ -32,183 +13,189 @@ const TaskEditor = ({ task, columnName, columnNames, dateFormat, vscode }: {
vscode: VSCodeApi vscode: VSCodeApi
}) => { }) => {
const editing = task !== null; 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 } }) => { // Called when the name field is changed
const updatePath = name.split("."); const handleUpdateName = ({ target: { value }}, values) => {
// Handle updating checkbox states (depends on previous state) // Update the id preview
if (type === 'checkbox') { values.id = paramCase(value);
setTaskData((previousState) => ({
[name]: !previousState[name]
}));
return;
}
// Handle updating root-level properties // Update the webview panel title
if (updatePath.length === 1) { vscode.postMessage({
const [key] = updatePath; command: 'kanbn.updatePanelTitle',
const newTaskData = { title: value || 'Untitled task'
[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 ( return (
<form className="kanbn-task-editor" onSubmit={handleSubmit}> <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'}</h1>
<div <Formik
style={{ initialValues={{
display: "flex" 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: []
}}
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';
// }
return errors;
}}
onSubmit={(values, { setSubmitting }) => {
if (editing) {
vscode.postMessage({
command: 'kanbn.update'
});
} else {
vscode.postMessage({
command: 'kanbn.create'
});
}
console.log(values);
setSubmitting(false);
}} }}
> >
<div className="kanbn-task-editor-column-left"> {({
<div className="kanbn-task-field kanbn-task-field-name"> values,
<label className="kanbn-task-field-label"> errors,
<p>Name</p> touched,
<input handleChange,
className="kanbn-task-field-input" handleBlur,
placeholder="Name" handleSubmit,
name="name" isSubmitting
value={taskData.name} }) => (
onChange={handleChange} <form onSubmit={handleSubmit}>
></input> <div
</label> style={{
<span className="kanbn-task-id">{taskData.id}</span> display: "flex"
</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 <div className="kanbn-task-editor-column-left">
</button> <div className="kanbn-task-field kanbn-task-field-name">
<button <label className="kanbn-task-field-label">
type="button" <p>Name</p>
className="kanbn-task-editor-button kanbn-task-editor-button-delete" <input
title="Delete" className="kanbn-task-field-input"
> placeholder="Name"
Delete name="name"
</button> value={values.name}
</div> onChange={e => {
<div className="kanbn-task-field kanbn-task-field-column"> handleChange(e);
<label className="kanbn-task-field-label"> handleUpdateName(e, values);
<p>Column</p> }}
<select onBlur={handleBlur}
className="kanbn-task-field-select" ></input>
name="column" </label>
value={taskData.column} {errors.name && touched.name && errors.name}
onChange={handleChange} <span className="kanbn-task-id">{values.id}</span>
> </div>
{columnNames.map(c => <option value={c}>{c}</option>)} <div className="kanbn-task-field kanbn-task-field-description">
</select> <label className="kanbn-task-field-label">
</label> <p>Description</p>
</div> <textarea
<div className="kanbn-task-field kanbn-task-field-assigned"> className="kanbn-task-field-textarea"
<label className="kanbn-task-field-label"> placeholder="Description"
<p>Assigned to</p> name="description"
<input value={values.description}
className="kanbn-task-field-input" onChange={handleChange}
name="metadata.assigned" onBlur={handleBlur}
value={taskData.metadata.assigned} ></textarea>
onChange={handleChange} </label>
></input> </div>
</label> </div>
</div> <div className="kanbn-task-editor-column-right">
<div className="kanbn-task-field kanbn-task-field-due"> <div>
<label className="kanbn-task-field-label"> <button
<p>Due date</p> type="submit"
<input className="kanbn-task-editor-button kanbn-task-editor-button-submit"
type="date" title="Save"
className="kanbn-task-field-input" disabled={isSubmitting}
name="metadata.due" >Save</button>
value={taskData.metadata.due} <button
onChange={handleChange} type="button"
></input> className="kanbn-task-editor-button kanbn-task-editor-button-delete"
</label> title="Delete"
</div> >Delete</button>
<div className="kanbn-task-field kanbn-task-field-progress"> </div>
<label className="kanbn-task-field-label"> <div className="kanbn-task-field kanbn-task-field-column">
<p>Progress</p> <label className="kanbn-task-field-label">
<input <p>Column</p>
type="number" <select
className="kanbn-task-field-input" className="kanbn-task-field-select"
name="progress" name="column"
value={taskData.progress} value={values.column}
onChange={handleChange} onChange={handleChange}
min="0" onBlur={handleBlur}
max="1" >
step="0.05" {columnNames.map(c => <option value={c}>{c}</option>)}
></input> </select>
</label> </label>
</div> </div>
</div> <div className="kanbn-task-field kanbn-task-field-assigned">
</div> <label className="kanbn-task-field-label">
</form> <p>Assigned to</p>
<input
className="kanbn-task-field-input"
placeholder="Assigned to"
name="metadata.assigned"
value={values.metadata.assigned}
onChange={handleChange}
onBlur={handleBlur}
></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={values.metadata.due}
onChange={handleChange}
onBlur={handleBlur}
></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={values.progress}
onChange={handleChange}
onBlur={handleBlur}
min="0"
max="1"
step="0.05"
></input>
</label>
</div>
</div>
</div>
</form>
)}
</Formik>
</div>
); );
} };
export default TaskEditor; export default TaskEditor;