Added task editing page

This commit is contained in:
Marta Borgia Leiva 2026-02-11 10:50:13 +01:00
parent ffc79a28dc
commit 69edc4e197
9 changed files with 322 additions and 2 deletions

View file

@ -54,6 +54,11 @@ export const routes: Routes = [
loadComponent: () => loadComponent: () =>
import('./pages/task-create/task-create.component').then((m) => m.TaskCreateComponent), import('./pages/task-create/task-create.component').then((m) => m.TaskCreateComponent),
}, },
{
path: 'projects/:id/tasks/:taskId/edit',
loadComponent: () =>
import('./pages/task-edit/task-edit.component').then((m) => m.TaskEditComponent),
},
], ],
}, },
]; ];

View file

@ -56,6 +56,26 @@
border-color: #fca5a5; border-color: #fca5a5;
} }
.btn-edit {
height: 32px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid #cbd5f0;
background: #ffffff;
color: #334155;
font-weight: 600;
font-size: 12px;
cursor: pointer;
transition:
background-color 0.2s ease,
border-color 0.2s ease;
}
.btn-edit:hover {
background-color: #f1f5f9;
border-color: #94a3b8;
}
.task-info h4 { .task-info h4 {
margin: 0 0 6px 0; margin: 0 0 6px 0;
color: #111827; color: #111827;

View file

@ -15,6 +15,7 @@
<option [value]="status">{{ status.replace('_', ' ') }}</option> <option [value]="status">{{ status.replace('_', ' ') }}</option>
} }
</select> </select>
<button class="btn-edit" type="button" aria-label="Edit task" (click)="onEdit()">Edit</button>
<button class="btn-remove" type="button" aria-label="Remove task" (click)="onRemove()"> <button class="btn-remove" type="button" aria-label="Remove task" (click)="onRemove()">
x x
</button> </button>

View file

@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Task } from '../../models/tasks.models'; import { Task } from '../../models/tasks.models';
@ -11,6 +12,7 @@ import { Task } from '../../models/tasks.models';
styleUrl: './task-item.component.css', styleUrl: './task-item.component.css',
}) })
export class TaskItemComponent { export class TaskItemComponent {
private router = inject(Router);
private _task!: Task; private _task!: Task;
statusValue: Task['status'] = 'pending'; statusValue: Task['status'] = 'pending';
@ -24,6 +26,9 @@ export class TaskItemComponent {
return this._task; return this._task;
} }
@Input()
projectId?: number;
@Output() statusChange = new EventEmitter<{ @Output() statusChange = new EventEmitter<{
task: Task; task: Task;
status: Task['status']; status: Task['status'];
@ -59,4 +64,14 @@ export class TaskItemComponent {
this.remove.emit(this._task); this.remove.emit(this._task);
} }
} }
onEdit() {
if (!this._task || this.projectId == null) {
return;
}
this.router.navigate(['/projects', this.projectId, 'tasks', this._task.id, 'edit'], {
state: { task: this._task },
});
}
} }

View file

@ -10,3 +10,9 @@ export interface CreateTaskRequest {
description?: string; description?: string;
status: Task['status']; status: Task['status'];
} }
export interface UpdateTaskRequest {
title: string;
description?: string;
status: Task['status'];
}

View file

@ -28,7 +28,11 @@
@if ((project()?.tasks?.length ?? 0) > 0) { @if ((project()?.tasks?.length ?? 0) > 0) {
<div class="tasks-list"> <div class="tasks-list">
@for (task of project()?.tasks ?? []; track task.id) { @for (task of project()?.tasks ?? []; track task.id) {
<app-task-item [task]="task" (statusChange)="onTaskStatusChange($event)" /> <app-task-item
[task]="task"
[projectId]="project()?.id"
(statusChange)="onTaskStatusChange($event)"
/>
} }
</div> </div>
} @else { } @else {

View file

@ -0,0 +1,99 @@
.create-task {
min-height: 100vh;
background-color: #f5f7fa;
padding: 40px;
display: flex;
justify-content: center;
align-items: flex-start;
}
.card {
width: 100%;
max-width: 640px;
height: fit-content;
background: white;
border-radius: 12px;
padding: 32px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card h1 {
margin: 0 0 24px 0;
font-size: 28px;
color: #2c3e50;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px 14px;
border: 1px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
background-color: #ffffff;
}
.form-group textarea {
resize: vertical;
}
.actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
}
.btn-primary {
padding: 10px 16px;
background-color: #10b981;
color: #fff;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
padding: 10px 16px;
background: none;
border: 1px solid #cbd5f0;
color: #334155;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.error {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background-color: #fee2e2;
color: #b91c1c;
}
.loading {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background-color: #e2e8f0;
color: #334155;
}

View file

@ -0,0 +1,60 @@
<div class="create-task">
<div class="card">
<h1>Edit task</h1>
@if (errorMessage()) {
<div class="error">{{ errorMessage() }}</div>
}
@if (isLoading()) {
<div class="loading">Loading task...</div>
} @else {
<form (ngSubmit)="onSubmit()" #taskForm="ngForm">
<div class="form-group">
<label for="title">Task title</label>
<input
id="title"
type="text"
name="title"
[(ngModel)]="title"
placeholder="Enter a task title"
required
/>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
name="description"
[(ngModel)]="description"
rows="4"
placeholder="Enter a task description"
></textarea>
</div>
<div class="form-group">
<label for="status">Status</label>
<select id="status" name="status" [(ngModel)]="status" required>
<option value="pending">Pending</option>
<option value="in_progress">In progress</option>
<option value="completed">Completed</option>
<option value="stashed">Stashed</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="actions">
<button type="button" class="btn-secondary" (click)="onCancel()">Cancel</button>
<button type="submit" class="btn-primary" [disabled]="isSaving() || !taskForm.form.valid">
@if (isSaving()) {
<span>Saving...</span>
} @else {
<span>Save changes</span>
}
</button>
</div>
</form>
}
</div>
</div>

View file

@ -0,0 +1,110 @@
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../../services/api.service';
import { Task, UpdateTaskRequest } from '../../models/tasks.models';
@Component({
selector: 'app-task-edit',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './task-edit.component.html',
styleUrls: ['./task-edit.component.css'],
})
export class TaskEditComponent {
private apiService = inject(ApiService);
private route = inject(ActivatedRoute);
private router = inject(Router);
title = '';
description = '';
status: Task['status'] = 'pending';
isLoading = signal(true);
isSaving = signal(false);
errorMessage = signal('');
private projectId: number | null = null;
private taskId: number | null = null;
constructor() {
const navState = this.router.getCurrentNavigation()?.extras.state ?? (history.state as any);
const initialTask = navState?.task as Task | undefined;
const projectParam = this.route.snapshot.paramMap.get('id');
const taskParam = this.route.snapshot.paramMap.get('taskId');
const projectId = projectParam ? Number(projectParam) : Number.NaN;
const taskId = taskParam ? Number(taskParam) : Number.NaN;
if (!Number.isFinite(projectId) || !Number.isFinite(taskId)) {
this.isLoading.set(false);
this.errorMessage.set('Invalid project or task id.');
return;
}
this.projectId = projectId;
this.taskId = taskId;
if (initialTask && initialTask.id === taskId) {
this.title = initialTask.title ?? '';
this.description = initialTask.description ?? '';
this.status = initialTask.status ?? 'pending';
this.isLoading.set(false);
return;
}
this.apiService.get<Task>(`/projects/${projectId}/tasks/${taskId}`).subscribe({
next: (task) => {
this.title = task.title ?? '';
this.description = task.description ?? '';
this.status = task.status ?? 'pending';
this.isLoading.set(false);
},
error: (error) => {
this.isLoading.set(false);
this.errorMessage.set(error?.error?.message || 'Failed to load task.');
},
});
}
onSubmit() {
if (!this.title.trim()) {
this.errorMessage.set('Task title is required.');
return;
}
if (this.projectId == null || this.taskId == null) {
this.errorMessage.set('Invalid project or task id.');
return;
}
const payload: UpdateTaskRequest = {
title: this.title.trim(),
description: this.description.trim() ? this.description.trim() : undefined,
status: this.status,
};
this.isSaving.set(true);
this.errorMessage.set('');
this.apiService
.put<Task>(`/projects/${this.projectId}/tasks/${this.taskId}`, payload)
.subscribe({
next: () => {
this.isSaving.set(false);
this.router.navigate(['/projects', this.projectId]);
},
error: (error) => {
this.isSaving.set(false);
this.errorMessage.set(error?.error?.message || 'Failed to update task.');
},
});
}
onCancel() {
if (this.projectId != null) {
this.router.navigate(['/projects', this.projectId]);
} else {
this.router.navigate(['/']);
}
}
}