Improve task editing and project closing functionality

Refactors the project template component to enable inline task editing, implement temporary task IDs for new tasks, and update the handling of closing projects to duplicate them as templates.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/P2PZNJ9
This commit is contained in:
paul-nothaft
2025-09-11 21:39:56 +00:00
parent 5827bb7992
commit b0a2f51927
2 changed files with 174 additions and 145 deletions
+174 -141
View File
@@ -273,9 +273,14 @@ export default function ProjectTemplate({
const handleSaveTask = () => {
if (editingTaskIndex !== null) {
const currentTask = projectTasks[editingTaskIndex];
const updatedTasks = [...projectTasks];
// Check if this is a new task (temporary ID) or existing task
const isNewTask = currentTask.id.startsWith('temp-');
updatedTasks[editingTaskIndex] = {
id: projectTasks[editingTaskIndex].id,
id: isNewTask ? Date.now().toString() : currentTask.id,
title: taskForm.title || 'New Task',
description: taskForm.description || '',
priority: taskForm.priority || 'medium',
@@ -283,6 +288,7 @@ export default function ProjectTemplate({
estimatedHours: taskForm.estimatedHours || 0,
labelId: taskForm.labelId
};
setProjectTasks(updatedTasks);
}
@@ -296,14 +302,41 @@ export default function ProjectTemplate({
};
const handleCancelTaskEdit = () => {
// If we're editing a task that has a temporary ID (new task), remove it from the list
if (editingTaskIndex !== null) {
const currentTask = projectTasks[editingTaskIndex];
if (currentTask.id.startsWith('temp-')) {
const updatedTasks = projectTasks.filter((_, index) => index !== editingTaskIndex);
setProjectTasks(updatedTasks);
}
}
setTaskForm({});
setIsEditingTask(false);
setEditingTaskIndex(null);
};
const handleStartNewTask = () => {
setTaskForm({});
setEditingTaskIndex(null);
// Create a temporary new task and add it to the list in edit mode
const tempTask: ProjectTask = {
id: 'temp-' + Date.now(),
title: '',
description: '',
priority: 'medium',
status: 'todo',
estimatedHours: 0
};
const newTasks = [...projectTasks, tempTask];
setProjectTasks(newTasks);
setEditingTaskIndex(newTasks.length - 1);
setTaskForm({
title: '',
description: '',
priority: 'medium',
status: 'todo',
estimatedHours: 0
});
setIsEditingTask(true);
};
@@ -962,49 +995,149 @@ export default function ProjectTemplate({
</Button>
</div>
{/* Task List */}
{/* Task List with Inline Editing */}
<div className="space-y-2 max-h-64 overflow-y-auto">
{projectTasks.map((task, index) => (
<Card key={task.id} className="p-3">
<div className="flex items-center justify-between">
<div className="flex-1">
<h4 className="font-medium text-sm">{task.title}</h4>
{task.description && (
<p className="text-xs text-muted-foreground">{task.description}</p>
)}
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{task.priority}
</Badge>
<Badge variant="outline" className="text-xs">
{task.status}
</Badge>
{task.estimatedHours ? (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h
</Badge>
) : null}
<Card key={task.id} className={`p-3 ${editingTaskIndex === index ? 'border-primary' : ''}`}>
{editingTaskIndex === index ? (
// Inline Edit Mode
<div className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">Task Title</label>
<Input
value={taskForm.title || ''}
onChange={(e) => setTaskForm({...taskForm, title: e.target.value})}
placeholder="Enter task title"
className="text-sm"
data-testid={`input-inline-task-title-${index}`}
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">Description</label>
<Input
value={taskForm.description || ''}
onChange={(e) => setTaskForm({...taskForm, description: e.target.value})}
placeholder="Enter task description"
className="text-sm"
data-testid={`input-inline-task-description-${index}`}
/>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">Priority</label>
<Select
value={taskForm.priority || 'medium'}
onValueChange={(value: ProjectTask['priority']) =>
setTaskForm({...taskForm, priority: value})
}
>
<SelectTrigger className="text-sm" data-testid={`select-inline-task-priority-${index}`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<label className="text-xs font-medium text-muted-foreground">Status</label>
<Select
value={taskForm.status || 'todo'}
onValueChange={(value: ProjectTask['status']) =>
setTaskForm({...taskForm, status: value})
}
>
<SelectTrigger className="text-sm" data-testid={`select-inline-task-status-${index}`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">To Do</SelectItem>
<SelectItem value="inProgress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1 md:col-span-2">
<label className="text-xs font-medium text-muted-foreground">Estimated Hours</label>
<Input
type="number"
value={taskForm.estimatedHours || ''}
onChange={(e) => setTaskForm({...taskForm, estimatedHours: Number(e.target.value)})}
placeholder="0"
min="0"
className="text-sm max-w-32"
data-testid={`input-inline-task-hours-${index}`}
/>
</div>
</div>
<div className="flex items-center gap-2 justify-end pt-2 border-t">
<Button
variant="outline"
size="sm"
onClick={handleCancelTaskEdit}
data-testid={`button-cancel-inline-edit-${index}`}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSaveTask}
disabled={!taskForm.title?.trim()}
data-testid={`button-save-inline-edit-${index}`}
>
Save Changes
</Button>
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditTask(index)}
data-testid={`button-edit-task-edit-${index}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTask(index)}
data-testid={`button-remove-task-edit-${index}`}
>
<Trash2 className="w-3 h-3" />
</Button>
) : (
// Display Mode
<div className="flex items-center justify-between">
<div className="flex-1">
<h4 className="font-medium text-sm">{task.title}</h4>
{task.description && (
<p className="text-xs text-muted-foreground">{task.description}</p>
)}
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{task.priority}
</Badge>
<Badge variant="outline" className="text-xs">
{task.status}
</Badge>
{task.estimatedHours ? (
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h
</Badge>
) : null}
</div>
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditTask(index)}
data-testid={`button-edit-task-edit-${index}`}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRemoveTask(index)}
data-testid={`button-remove-task-edit-${index}`}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
</div>
)}
</Card>
))}
@@ -1015,106 +1148,6 @@ export default function ProjectTemplate({
)}
</div>
{/* Task Form */}
{isEditingTask && (
<Card className="p-4 border-primary">
<div className="space-y-4">
<h4 className="font-semibold text-sm">
{editingTaskIndex !== null ? 'Edit Task' : 'New Task'}
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Task Title</label>
<Input
value={taskForm.title || ''}
onChange={(e) => setTaskForm({...taskForm, title: e.target.value})}
placeholder="Enter task title"
data-testid="input-task-title-edit"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description</label>
<Input
value={taskForm.description || ''}
onChange={(e) => setTaskForm({...taskForm, description: e.target.value})}
placeholder="Enter task description"
data-testid="input-task-description-edit"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Priority</label>
<Select
value={taskForm.priority || 'medium'}
onValueChange={(value: ProjectTask['priority']) =>
setTaskForm({...taskForm, priority: value})
}
>
<SelectTrigger data-testid="select-task-priority-edit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Status</label>
<Select
value={taskForm.status || 'todo'}
onValueChange={(value: ProjectTask['status']) =>
setTaskForm({...taskForm, status: value})
}
>
<SelectTrigger data-testid="select-task-status-edit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">To Do</SelectItem>
<SelectItem value="inProgress">In Progress</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Estimated Hours</label>
<Input
type="number"
value={taskForm.estimatedHours || ''}
onChange={(e) => setTaskForm({...taskForm, estimatedHours: Number(e.target.value)})}
placeholder="0"
min="0"
data-testid="input-task-hours-edit"
/>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={handleCancelTaskEdit}
className="flex-1"
>
Cancel
</Button>
<Button
onClick={editingTaskIndex !== null ? handleSaveTask : handleAddTask}
disabled={!taskForm.title?.trim()}
className="flex-1"
data-testid="button-save-task-edit"
>
{editingTaskIndex !== null ? 'Save Changes' : 'Add Task'}
</Button>
</div>
</div>
</Card>
)}
</div>
</div>
)}