import React, { useState } from 'react'; import { Formik, Form, Field, ErrorMessage, FieldArray } from 'formik'; import formatDate from 'dateformat'; import VSCodeApi from './VSCodeApi'; import { paramCase } from 'param-case'; import gitUsername from 'git-user-name'; import ReactMarkdown from 'react-markdown'; import TextareaAutosize from 'react-textarea-autosize'; interface KanbnTaskValidationOutput { name: string, metadata: { tags: string[] }, subTasks: Array<{ text: string }>, comments: Array<{ text: string }> } interface KanbnTaskValidationInput extends KanbnTaskValidationOutput { id: string } const TaskEditor = ({ task, tasks, columnName, columnNames, dateFormat, panelUuid, vscode }: { task: KanbnTask | null, tasks: Record, columnName: string, columnNames: string[], dateFormat: string, panelUuid: string, vscode: VSCodeApi }) => { const editing = task !== null; const [taskData, setTaskData] = useState({ id: task ? task.id : '', name: task ? task.name : '', description: task ? task.description : '', column: columnName, progress: task ? task.progress : 0, metadata: { created: (task && 'created' in task.metadata) ? task.metadata.created : new Date(), updated: (task && 'updated' in task.metadata) ? task.metadata.updated : null, 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 || []) : [] }, relations: task ? task.relations : [], subTasks: task ? task.subTasks : [], comments: task ? task.comments : [] }); const [editingDescription, setEditingDescription] = useState(!editing); const [editingComment, setEditingComment] = useState(-1); // Called when the name field is changed const handleUpdateName = ({ target: { value } }, values) => { const id = paramCase(value); // Update the id preview setTaskData({ ...taskData, id }); // Update values values.id = id; // Update the webview panel title vscode.postMessage({ command: 'kanbn.updatePanelTitle', title: value || 'Untitled task' }); }; // Called when the form is submitted const handleSubmit = (values, setSubmitting, resetForm) => { if (editing) { vscode.postMessage({ command: 'kanbn.update', taskId: task!.id, taskData: values, panelUuid }); } else { vscode.postMessage({ command: 'kanbn.create', taskData: values, panelUuid }); } setTaskData(values); resetForm({ values }); setSubmitting(false); }; // Called when the delete task button is clicked const handleRemoveTask = values => { vscode.postMessage({ command: 'kanbn.delete', taskId: task!.id, taskData: values, panelUuid }); }; // 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; }; // Validate form data const validate = (values: KanbnTaskValidationInput): KanbnTaskValidationOutput | {} => { let hasErrors = false; const errors: KanbnTaskValidationOutput = { name: '', metadata: { tags: [] }, subTasks: [], comments: [] }; // Task name cannot be empty if (!values.name) { errors.name = 'Task name is required.'; hasErrors = true; } // Check if the id is already in use if (taskData.id in tasks && tasks[taskData.id].uuid !== (task ? task.uuid : '')) { errors.name = 'There is already a task with the same name or id.'; hasErrors = true; } // 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.'; hasErrors = true; } } // 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.' }; hasErrors = true; } } // 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.' }; hasErrors = true; } } return hasErrors ? errors : {}; }; return (
{ handleSubmit(values, setSubmitting, resetForm); }} > {({ dirty, values, handleChange, isSubmitting }) => (

{editing ? 'Update task' : 'Create new task'} {dirty && *} {editing && { [ '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(', ') } }

{taskData.id}
{ editingDescription ? : {values.description} }

Sub-tasks

{({ insert, remove, push }) => (
{values.subTasks.length > 0 && values.subTasks.map((subTask, index) => (
))}
)}

Relations

{({ insert, remove, push }) => (
{values.relations.length > 0 && values.relations.map((relation, index) => (
{Object.keys(tasks).map(t => )}
))}
)}

Comments

{({ insert, remove, push }) => (
{values.comments.length > 0 && values.comments.map((comment, index) => (
{ editingComment === index ? :
{comment.author || 'Anonymous'}
}
{formatDate(comment.date, dateFormat)}
{ editingComment === index ? : {comment.text} }
))}
)}
{editing && }
{({ insert, remove, push }) => (
{( 'tags' in values.metadata && values.metadata.tags!.length > 0 ) && values.metadata.tags!.map((tag, index) => (
))}
)}
)}
); }; export default TaskEditor;