Clean up: Remove component files
This commit is contained in:
parent
c6853c6f0e
commit
78faf60591
@ -1,297 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import {
|
|
||||||
DESCRIPTION_MAX_LENGTH,
|
|
||||||
PRIORITY_LABELS,
|
|
||||||
STATUS_LABELS,
|
|
||||||
TASK_PRIORITIES,
|
|
||||||
TASK_STATUSES,
|
|
||||||
Task,
|
|
||||||
TaskDraft,
|
|
||||||
TaskPriority,
|
|
||||||
TaskStatus,
|
|
||||||
TITLE_MAX_LENGTH,
|
|
||||||
User,
|
|
||||||
} from '../../types';
|
|
||||||
import './TaskForm.css';
|
|
||||||
|
|
||||||
interface TaskFormProps {
|
|
||||||
users: User[];
|
|
||||||
/** Task being edited, or null to create a new one. */
|
|
||||||
task: Task | null;
|
|
||||||
onCancel: () => void;
|
|
||||||
/** Rejects with a user-safe message; the form stays open so nothing is lost. */
|
|
||||||
onSubmit: (draft: TaskDraft) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormValues {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
assignee_id: string;
|
|
||||||
status: TaskStatus;
|
|
||||||
priority: TaskPriority;
|
|
||||||
due_date: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FOCUSABLE =
|
|
||||||
'button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [href]';
|
|
||||||
|
|
||||||
const TaskForm: React.FC<TaskFormProps> = ({ users, task, onCancel, onSubmit }) => {
|
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setFocus,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
} = useForm<FormValues>({
|
|
||||||
defaultValues: {
|
|
||||||
title: task?.title ?? '',
|
|
||||||
description: task?.description ?? '',
|
|
||||||
assignee_id: task?.assigned_to ? String(task.assigned_to.id) : '',
|
|
||||||
status: task?.status ?? 'open',
|
|
||||||
priority: task?.priority ?? 'medium',
|
|
||||||
due_date: task?.due_date ?? '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFocus('title');
|
|
||||||
}, [setFocus]);
|
|
||||||
|
|
||||||
const submit = async (values: FormValues): Promise<void> => {
|
|
||||||
setSubmitError(null);
|
|
||||||
try {
|
|
||||||
await onSubmit({
|
|
||||||
title: values.title,
|
|
||||||
description: values.description,
|
|
||||||
assignee_id: Number(values.assignee_id),
|
|
||||||
status: values.status,
|
|
||||||
priority: values.priority,
|
|
||||||
due_date: values.due_date,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setSubmitError(
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'The task could not be saved. Your details are still here, so you can try again.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Escape closes; Tab stays inside the dialog. */
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.stopPropagation();
|
|
||||||
onCancel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key !== 'Tab' || !dialogRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const focusable = Array.from(dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE));
|
|
||||||
if (focusable.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const first = focusable[0];
|
|
||||||
const last = focusable[focusable.length - 1];
|
|
||||||
|
|
||||||
if (event.shiftKey && document.activeElement === first) {
|
|
||||||
event.preventDefault();
|
|
||||||
last.focus();
|
|
||||||
} else if (!event.shiftKey && document.activeElement === last) {
|
|
||||||
event.preventDefault();
|
|
||||||
first.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const titleId = 'task-form-title';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="task-form-overlay">
|
|
||||||
<div
|
|
||||||
className="task-form-container"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby={titleId}
|
|
||||||
ref={dialogRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
<form className="task-form" onSubmit={handleSubmit(submit)} noValidate>
|
|
||||||
<div className="form-header">
|
|
||||||
<h2 id={titleId}>{task ? 'Edit Task' : 'Create New Task'}</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="close-btn"
|
|
||||||
onClick={onCancel}
|
|
||||||
aria-label="Close without saving"
|
|
||||||
>
|
|
||||||
<span className="close-text">Close</span>
|
|
||||||
<span aria-hidden="true">✕</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{submitError && (
|
|
||||||
<p className="form-error-banner" role="alert">
|
|
||||||
{submitError}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label required" htmlFor="task-title">
|
|
||||||
Task Title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="task-title"
|
|
||||||
type="text"
|
|
||||||
className="form-input"
|
|
||||||
placeholder="e.g., Update client onboarding documentation"
|
|
||||||
aria-required="true"
|
|
||||||
aria-invalid={errors.title ? 'true' : 'false'}
|
|
||||||
aria-describedby={errors.title ? 'task-title-error' : undefined}
|
|
||||||
{...register('title', {
|
|
||||||
required: 'Enter a task title so your team knows what to do.',
|
|
||||||
maxLength: {
|
|
||||||
value: TITLE_MAX_LENGTH,
|
|
||||||
message: `Keep the title under ${TITLE_MAX_LENGTH} characters.`,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{errors.title ? (
|
|
||||||
<p className="field-error" id="task-title-error" role="alert">
|
|
||||||
{errors.title.message}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="form-hint">
|
|
||||||
<span className="required-marker" aria-hidden="true">
|
|
||||||
*
|
|
||||||
</span>{' '}
|
|
||||||
Required field
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label" htmlFor="task-description">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="task-description"
|
|
||||||
className="form-textarea"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Enter detailed task description..."
|
|
||||||
aria-invalid={errors.description ? 'true' : 'false'}
|
|
||||||
aria-describedby={errors.description ? 'task-description-error' : undefined}
|
|
||||||
{...register('description', {
|
|
||||||
maxLength: {
|
|
||||||
value: DESCRIPTION_MAX_LENGTH,
|
|
||||||
message: `Keep the description under ${DESCRIPTION_MAX_LENGTH} characters.`,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
{errors.description && (
|
|
||||||
<p className="field-error" id="task-description-error" role="alert">
|
|
||||||
{errors.description.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label required" htmlFor="task-assignee">
|
|
||||||
Assignee
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="task-assignee"
|
|
||||||
className="form-select"
|
|
||||||
aria-required="true"
|
|
||||||
aria-invalid={errors.assignee_id ? 'true' : 'false'}
|
|
||||||
aria-describedby={errors.assignee_id ? 'task-assignee-error' : undefined}
|
|
||||||
{...register('assignee_id', {
|
|
||||||
required: 'Choose the team member who owns this task.',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<option value="">Select assignee...</option>
|
|
||||||
{users.map((user) => (
|
|
||||||
<option key={user.id} value={user.id}>
|
|
||||||
{user.full_name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{errors.assignee_id && (
|
|
||||||
<p className="field-error" id="task-assignee-error" role="alert">
|
|
||||||
{errors.assignee_id.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset className="form-group status-fieldset">
|
|
||||||
<legend className="form-label required">Status</legend>
|
|
||||||
<div className="status-buttons">
|
|
||||||
{TASK_STATUSES.map((status) => (
|
|
||||||
<label key={status} className="status-option">
|
|
||||||
<input type="radio" value={status} {...register('status')} />
|
|
||||||
<span className={`status-pill ${status}`}>{STATUS_LABELS[status]}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<div className="form-row">
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label required" htmlFor="task-due-date">
|
|
||||||
Due Date
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="task-due-date"
|
|
||||||
type="date"
|
|
||||||
className="form-input"
|
|
||||||
aria-required="true"
|
|
||||||
aria-invalid={errors.due_date ? 'true' : 'false'}
|
|
||||||
aria-describedby={errors.due_date ? 'task-due-date-error' : undefined}
|
|
||||||
{...register('due_date', { required: 'Pick a due date for this task.' })}
|
|
||||||
/>
|
|
||||||
{errors.due_date && (
|
|
||||||
<p className="field-error" id="task-due-date-error" role="alert">
|
|
||||||
{errors.due_date.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-group">
|
|
||||||
<label className="form-label required" htmlFor="task-priority">
|
|
||||||
Priority
|
|
||||||
</label>
|
|
||||||
<select id="task-priority" className="form-select" {...register('priority')}>
|
|
||||||
{TASK_PRIORITIES.map((priority) => (
|
|
||||||
<option key={priority} value={priority}>
|
|
||||||
{PRIORITY_LABELS[priority]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-actions">
|
|
||||||
<button type="submit" className="btn btn-primary" disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? 'Saving...' : 'Save Task'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TaskForm;
|
|
||||||
Loading…
Reference in New Issue
Block a user