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
-4
View File
@@ -14,10 +14,6 @@ run = ["npm", "run", "start"]
localPort = 5000 localPort = 5000
externalPort = 80 externalPort = 80
[[ports]]
localPort = 33729
externalPort = 3001
[[ports]] [[ports]]
localPort = 35345 localPort = 35345
externalPort = 3002 externalPort = 3002
+174 -141
View File
@@ -273,9 +273,14 @@ export default function ProjectTemplate({
const handleSaveTask = () => { const handleSaveTask = () => {
if (editingTaskIndex !== null) { if (editingTaskIndex !== null) {
const currentTask = projectTasks[editingTaskIndex];
const updatedTasks = [...projectTasks]; const updatedTasks = [...projectTasks];
// Check if this is a new task (temporary ID) or existing task
const isNewTask = currentTask.id.startsWith('temp-');
updatedTasks[editingTaskIndex] = { updatedTasks[editingTaskIndex] = {
id: projectTasks[editingTaskIndex].id, id: isNewTask ? Date.now().toString() : currentTask.id,
title: taskForm.title || 'New Task', title: taskForm.title || 'New Task',
description: taskForm.description || '', description: taskForm.description || '',
priority: taskForm.priority || 'medium', priority: taskForm.priority || 'medium',
@@ -283,6 +288,7 @@ export default function ProjectTemplate({
estimatedHours: taskForm.estimatedHours || 0, estimatedHours: taskForm.estimatedHours || 0,
labelId: taskForm.labelId labelId: taskForm.labelId
}; };
setProjectTasks(updatedTasks); setProjectTasks(updatedTasks);
} }
@@ -296,14 +302,41 @@ export default function ProjectTemplate({
}; };
const handleCancelTaskEdit = () => { 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({}); setTaskForm({});
setIsEditingTask(false); setIsEditingTask(false);
setEditingTaskIndex(null); setEditingTaskIndex(null);
}; };
const handleStartNewTask = () => { const handleStartNewTask = () => {
setTaskForm({}); // Create a temporary new task and add it to the list in edit mode
setEditingTaskIndex(null); 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); setIsEditingTask(true);
}; };
@@ -962,49 +995,149 @@ export default function ProjectTemplate({
</Button> </Button>
</div> </div>
{/* Task List */} {/* Task List with Inline Editing */}
<div className="space-y-2 max-h-64 overflow-y-auto"> <div className="space-y-2 max-h-64 overflow-y-auto">
{projectTasks.map((task, index) => ( {projectTasks.map((task, index) => (
<Card key={task.id} className="p-3"> <Card key={task.id} className={`p-3 ${editingTaskIndex === index ? 'border-primary' : ''}`}>
<div className="flex items-center justify-between"> {editingTaskIndex === index ? (
<div className="flex-1"> // Inline Edit Mode
<h4 className="font-medium text-sm">{task.title}</h4> <div className="space-y-3">
{task.description && ( <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<p className="text-xs text-muted-foreground">{task.description}</p> <div className="space-y-1">
)} <label className="text-xs font-medium text-muted-foreground">Task Title</label>
<div className="flex items-center gap-2 mt-1"> <Input
<Badge variant="outline" className="text-xs"> value={taskForm.title || ''}
{task.priority} onChange={(e) => setTaskForm({...taskForm, title: e.target.value})}
</Badge> placeholder="Enter task title"
<Badge variant="outline" className="text-xs"> className="text-sm"
{task.status} data-testid={`input-inline-task-title-${index}`}
</Badge> />
{task.estimatedHours ? ( </div>
<Badge variant="outline" className="text-xs">
{task.estimatedHours}h <div className="space-y-1">
</Badge> <label className="text-xs font-medium text-muted-foreground">Description</label>
) : null} <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> </div>
<div className="flex items-center gap-1"> ) : (
<Button // Display Mode
variant="ghost" <div className="flex items-center justify-between">
size="sm" <div className="flex-1">
onClick={() => handleEditTask(index)} <h4 className="font-medium text-sm">{task.title}</h4>
data-testid={`button-edit-task-edit-${index}`} {task.description && (
> <p className="text-xs text-muted-foreground">{task.description}</p>
<Edit className="w-3 h-3" /> )}
</Button> <div className="flex items-center gap-2 mt-1">
<Button <Badge variant="outline" className="text-xs">
variant="ghost" {task.priority}
size="sm" </Badge>
onClick={() => handleRemoveTask(index)} <Badge variant="outline" className="text-xs">
data-testid={`button-remove-task-edit-${index}`} {task.status}
> </Badge>
<Trash2 className="w-3 h-3" /> {task.estimatedHours ? (
</Button> <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>
</div> )}
</Card> </Card>
))} ))}
@@ -1015,106 +1148,6 @@ export default function ProjectTemplate({
)} )}
</div> </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>
</div> </div>
)} )}