mirror of
https://github.com/a-mayb3/KanbanCloneAngular.git
synced 2026-03-21 18:05:38 +01:00
Added task editing page
This commit is contained in:
parent
ffc79a28dc
commit
69edc4e197
9 changed files with 322 additions and 2 deletions
|
|
@ -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),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
99
src/app/pages/task-edit/task-edit.component.css
Normal file
99
src/app/pages/task-edit/task-edit.component.css
Normal 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;
|
||||||
|
}
|
||||||
60
src/app/pages/task-edit/task-edit.component.html
Normal file
60
src/app/pages/task-edit/task-edit.component.html
Normal 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>
|
||||||
110
src/app/pages/task-edit/task-edit.component.ts
Normal file
110
src/app/pages/task-edit/task-edit.component.ts
Normal 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(['/']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue