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