Skip to content

Commit 6468f56

Browse files
committed
feat(tasks): add secure task deletion flows (kanban/all-tasks/my-tasks) with PrimeNG confirm dialogs
1 parent 82b56fe commit 6468f56

8 files changed

Lines changed: 187 additions & 6 deletions

File tree

src/app/app.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { provideHttpClient, withInterceptors } from '@angular/common/http';
44

55
import { routes } from './app.routes';
66
import { provideAnimations } from '@angular/platform-browser/animations';
7-
import { MessageService } from 'primeng/api';
7+
import { ConfirmationService, MessageService } from 'primeng/api';
88
import { environment } from '../environments/environment';
99
import { APP_ENVIRONMENT } from './core/config/app-environment.token';
1010
import { authInterceptor } from './core/auth/interceptors/auth.interceptor';
@@ -16,6 +16,7 @@ export const appConfig: ApplicationConfig = {
1616
provideRouter(routes),
1717
provideHttpClient(withInterceptors([authInterceptor, problemDetailsInterceptor])),
1818
provideAnimations(),
19+
ConfirmationService,
1920
MessageService,
2021
{
2122
provide: APP_ENVIRONMENT,

src/app/features/projects/components/project-kanban/project-kanban.component.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div class="kanban-page">
2+
<p-confirmDialog [baseZIndex]="1300" [appendTo]="'body'"></p-confirmDialog>
3+
24
<p-card class="kanban-header-card">
35
<div class="kanban-toolbar">
46
<div class="kanban-toolbar__title">
@@ -101,7 +103,7 @@ <h2>Project Kanban Board</h2>
101103
<div class="kanban-column-body">
102104
<div class="task-stack">
103105
<div
104-
*ngIf="getColumnTaskCount(column.status) === 0"
106+
*ngIf="getColumnTaskCount(column.status) === 0 && selectedProjectTaskCount > 0"
105107
class="kanban-empty-drop-target"
106108
[class.kanban-empty-drop-target--active]="isDropSlotActive(column.status, 0)"
107109
(dragover)="onDropSlotDragOver(column.status, 0, $event)"
@@ -156,6 +158,14 @@ <h2>Project Kanban Board</h2>
156158
<div class="task-card__header-actions">
157159
<p-tag *ngIf="isTaskPending(task.id)" severity="info" value="Saving"></p-tag>
158160
<button pButton type="button" icon="pi pi-pencil" class="p-button-rounded p-button-text p-button-sm" [disabled]="isTaskPending(task.id)" (click)="openEditTask(task)"></button>
161+
<button
162+
pButton
163+
type="button"
164+
icon="pi pi-trash"
165+
class="p-button-rounded p-button-text p-button-sm p-button-danger"
166+
[disabled]="!canDeleteTask(task) || isTaskPending(task.id)"
167+
(click)="deleteTask(task)">
168+
</button>
159169
</div>
160170
</div>
161171

@@ -178,6 +188,7 @@ <h2>Project Kanban Board</h2>
178188
</ng-container>
179189

180190
<div
191+
*ngIf="selectedProjectTaskCount > 0"
181192
class="kanban-drop-slot kanban-drop-slot--end"
182193
[class.kanban-drop-slot--active]="isDropSlotActive(column.status, getColumnTaskCount(column.status))"
183194
(dragover)="onDropSlotDragOver(column.status, getColumnTaskCount(column.status), $event)"

src/app/features/projects/components/project-kanban/project-kanban.component.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
33
import { ActivatedRoute, Router } from '@angular/router';
4-
import { Message, MessageService } from 'primeng/api';
4+
import { ConfirmationService, Message, MessageService } from 'primeng/api';
55
import { DialogModule } from 'primeng/dialog';
66
import { InputTextareaModule } from 'primeng/inputtextarea';
77
import { Subject, forkJoin, takeUntil } from 'rxjs';
@@ -57,6 +57,7 @@ export class ProjectKanbanComponent implements OnInit, OnDestroy {
5757
private readonly activatedRoute = inject(ActivatedRoute);
5858
private readonly router = inject(Router);
5959
private readonly messageService = inject(MessageService);
60+
private readonly confirmationService = inject(ConfirmationService);
6061
private readonly authService = inject(AuthService);
6162
private readonly appEnvironment = inject(APP_ENVIRONMENT);
6263
private readonly preferencesService = inject(AppPreferencesService);
@@ -138,6 +139,14 @@ export class ProjectKanbanComponent implements OnInit, OnDestroy {
138139
return this.selectedProjectIndex >= 0 && this.selectedProjectIndex < this.projects.length - 1;
139140
}
140141

142+
get currentUserId(): string | null {
143+
return this.authService.currentUserId();
144+
}
145+
146+
get canManageAllTasks(): boolean {
147+
return this.authService.hasAnyRole(['Administrator', 'ProjectManager']);
148+
}
149+
141150
ngOnInit(): void {
142151
this.loadProjects();
143152
}
@@ -322,6 +331,55 @@ export class ProjectKanbanComponent implements OnInit, OnDestroy {
322331
});
323332
}
324333

334+
isAssignedToMe(task: TaskItemDto): boolean {
335+
return !!this.currentUserId && task.assignedUserId === this.currentUserId;
336+
}
337+
338+
canDeleteTask(task: TaskItemDto): boolean {
339+
return this.canManageAllTasks || this.isAssignedToMe(task);
340+
}
341+
342+
deleteTask(task: TaskItemDto): void {
343+
if (!this.canDeleteTask(task) || this.isTaskPending(task.id)) {
344+
return;
345+
}
346+
347+
this.confirmationService.confirm({
348+
header: 'Delete Task',
349+
message: `Delete task "${task.title}"? This action cannot be undone.`,
350+
icon: 'pi pi-exclamation-triangle',
351+
acceptLabel: 'Delete',
352+
rejectLabel: 'Cancel',
353+
acceptButtonStyleClass: 'p-button-danger',
354+
accept: () => {
355+
const previousTasksSnapshot = this.allTasks.map((entry) => ({ ...entry }));
356+
this.pendingTaskIds.add(task.id);
357+
this.setAllTasks(this.allTasks.filter((entry) => entry.id !== task.id));
358+
359+
if (this.isPreviewMode) {
360+
this.pendingTaskIds.delete(task.id);
361+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted in preview mode.' });
362+
return;
363+
}
364+
365+
this.taskItemsApiClient
366+
.delete(task.id)
367+
.pipe(takeUntil(this.destroy$))
368+
.subscribe({
369+
next: () => {
370+
this.pendingTaskIds.delete(task.id);
371+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted successfully.' });
372+
},
373+
error: () => {
374+
this.pendingTaskIds.delete(task.id);
375+
this.setAllTasks(previousTasksSnapshot);
376+
this.messageService.add({ severity: 'error', summary: 'Delete failed', detail: 'Could not delete task.' });
377+
}
378+
});
379+
}
380+
});
381+
}
382+
325383
getTasksByStatus(status: TaskStatus): TaskItemDto[] {
326384
return this.columnTasks[status];
327385
}

src/app/features/task-item/components/task-item-list/task-item-list.component.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div class="tasks-page">
2+
<p-confirmDialog [baseZIndex]="1300" [appendTo]="'body'"></p-confirmDialog>
3+
24
<p-card class="tasks-header-card">
35
<div class="tasks-header">
46
<div class="tasks-header__title">
@@ -163,6 +165,16 @@ <h2>All Tasks</h2>
163165
(click)="unassignTask(task)"
164166
pTooltip="Unassign">
165167
</button>
168+
169+
<button
170+
pButton
171+
type="button"
172+
icon="pi pi-trash"
173+
class="p-button-rounded p-button-text p-button-danger"
174+
[disabled]="!canDeleteTask(task) || isTaskPending(task.id)"
175+
(click)="deleteTask(task)"
176+
pTooltip="Delete task">
177+
</button>
166178
</div>
167179
</td>
168180
</tr>

src/app/features/task-item/components/task-item-list/task-item-list.component.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
22
import { Router } from '@angular/router';
3-
import { Message, MessageService } from 'primeng/api';
3+
import { ConfirmationService, Message, MessageService } from 'primeng/api';
44
import { Table } from 'primeng/table';
55
import { Subject, takeUntil } from 'rxjs';
66
import { ProjectsApiClient } from '../../../../core/api/clients/projects-api.client';
@@ -36,6 +36,7 @@ export class TaskItemListComponent implements OnInit, OnDestroy {
3636
private readonly preferencesService = inject(AppPreferencesService);
3737
private readonly router = inject(Router);
3838
private readonly messageService = inject(MessageService);
39+
private readonly confirmationService = inject(ConfirmationService);
3940
private readonly destroy$ = new Subject<void>();
4041

4142
readonly statusOptions: StatusOption[] = [
@@ -213,6 +214,10 @@ export class TaskItemListComponent implements OnInit, OnDestroy {
213214
return this.canManageAllTasks || this.isAssignedToMe(task);
214215
}
215216

217+
canDeleteTask(task: TaskItemDto): boolean {
218+
return this.canManageAllTasks || this.isAssignedToMe(task);
219+
}
220+
216221
isTaskPending(taskId: string): boolean {
217222
return this.pendingTaskIds.has(taskId);
218223
}
@@ -286,6 +291,47 @@ export class TaskItemListComponent implements OnInit, OnDestroy {
286291
);
287292
}
288293

294+
deleteTask(task: TaskItemDto): void {
295+
if (!this.canDeleteTask(task) || this.isTaskPending(task.id)) {
296+
return;
297+
}
298+
299+
this.confirmationService.confirm({
300+
header: 'Delete Task',
301+
message: `Delete task "${task.title}"? This action cannot be undone.`,
302+
icon: 'pi pi-exclamation-triangle',
303+
acceptLabel: 'Delete',
304+
rejectLabel: 'Cancel',
305+
acceptButtonStyleClass: 'p-button-danger',
306+
accept: () => {
307+
const previousTasks = [...this.tasks];
308+
this.pendingTaskIds.add(task.id);
309+
this.tasks = this.tasks.filter((entry) => entry.id !== task.id);
310+
311+
if (this.isPreviewMode) {
312+
this.pendingTaskIds.delete(task.id);
313+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted in preview mode.' });
314+
return;
315+
}
316+
317+
this.taskItemsApiClient
318+
.delete(task.id)
319+
.pipe(takeUntil(this.destroy$))
320+
.subscribe({
321+
next: () => {
322+
this.pendingTaskIds.delete(task.id);
323+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted successfully.' });
324+
},
325+
error: () => {
326+
this.pendingTaskIds.delete(task.id);
327+
this.tasks = previousTasks;
328+
this.messageService.add({ severity: 'error', summary: 'Delete Failed', detail: 'Could not delete task.' });
329+
}
330+
});
331+
}
332+
});
333+
}
334+
289335
private loadProjects(): void {
290336
this.isLoadingProjects = true;
291337
this.projects = [];

src/app/features/task-item/components/user-task-items/user-task-items.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<div class="my-tasks-page">
2+
<p-confirmDialog [baseZIndex]="1300" [appendTo]="'body'"></p-confirmDialog>
3+
24
<p-card class="my-tasks-header-card">
35
<div class="my-tasks-header">
46
<div class="my-tasks-header__title">
@@ -70,6 +72,7 @@ <h3>To Do</h3>
7072
<div class="task-actions">
7173
<button pButton type="button" icon="pi pi-pencil" class="p-button-text p-button-rounded" pTooltip="Edit task" tooltipPosition="top" (click)="openEditTask(task)" [disabled]="!canEditTask(task) || isTaskPending(task.id)"></button>
7274
<button pButton type="button" icon="pi pi-th-large" class="p-button-text p-button-rounded" pTooltip="Open project board" tooltipPosition="top" (click)="openTaskBoard(task)"></button>
75+
<button pButton type="button" icon="pi pi-trash" class="p-button-text p-button-rounded p-button-danger" pTooltip="Delete task" tooltipPosition="top" (click)="deleteTask(task)" [disabled]="!canDeleteTask(task) || isTaskPending(task.id)"></button>
7376
</div>
7477
</li>
7578
<li *ngIf="todoTasks.length === 0" class="task-empty-row">No tasks in To Do.</li>
@@ -108,6 +111,7 @@ <h3>In Progress</h3>
108111
<div class="task-actions">
109112
<button pButton type="button" icon="pi pi-pencil" class="p-button-text p-button-rounded" pTooltip="Edit task" tooltipPosition="top" (click)="openEditTask(task)" [disabled]="!canEditTask(task) || isTaskPending(task.id)"></button>
110113
<button pButton type="button" icon="pi pi-th-large" class="p-button-text p-button-rounded" pTooltip="Open project board" tooltipPosition="top" (click)="openTaskBoard(task)"></button>
114+
<button pButton type="button" icon="pi pi-trash" class="p-button-text p-button-rounded p-button-danger" pTooltip="Delete task" tooltipPosition="top" (click)="deleteTask(task)" [disabled]="!canDeleteTask(task) || isTaskPending(task.id)"></button>
111115
</div>
112116
</li>
113117
<li *ngIf="inProgressTasks.length === 0" class="task-empty-row">No tasks currently in progress.</li>
@@ -143,6 +147,7 @@ <h3>Done</h3>
143147
<div class="task-actions">
144148
<button pButton type="button" icon="pi pi-pencil" class="p-button-text p-button-rounded" pTooltip="Edit task" tooltipPosition="top" (click)="openEditTask(task)" [disabled]="!canEditTask(task) || isTaskPending(task.id)"></button>
145149
<button pButton type="button" icon="pi pi-th-large" class="p-button-text p-button-rounded" pTooltip="Open project board" tooltipPosition="top" (click)="openTaskBoard(task)"></button>
150+
<button pButton type="button" icon="pi pi-trash" class="p-button-text p-button-rounded p-button-danger" pTooltip="Delete task" tooltipPosition="top" (click)="deleteTask(task)" [disabled]="!canDeleteTask(task) || isTaskPending(task.id)"></button>
146151
</div>
147152
</li>
148153
<li *ngIf="doneTasks.length === 0" class="task-empty-row">No completed tasks yet.</li>

src/app/features/task-item/components/user-task-items/user-task-items.component.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
22
import { Router } from '@angular/router';
3-
import { Message, MessageService } from 'primeng/api';
3+
import { ConfirmationService, Message, MessageService } from 'primeng/api';
44
import { DialogModule } from 'primeng/dialog';
55
import { InputTextareaModule } from 'primeng/inputtextarea';
66
import { MessagesModule } from 'primeng/messages';
@@ -35,6 +35,7 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
3535
private readonly appEnvironment = inject(APP_ENVIRONMENT);
3636
private readonly preferencesService = inject(AppPreferencesService);
3737
private readonly messageService = inject(MessageService);
38+
private readonly confirmationService = inject(ConfirmationService);
3839
private readonly router = inject(Router);
3940
private readonly destroy$ = new Subject<void>();
4041

@@ -140,6 +141,10 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
140141
return !!this.authService.currentUserId() && task.assignedUserId === this.authService.currentUserId();
141142
}
142143

144+
canDeleteTask(task: TaskItemDto): boolean {
145+
return this.canEditTask(task);
146+
}
147+
143148
getTagSeverity(status: TaskStatus): TagSeverity {
144149
switch (status) {
145150
case TaskStatus.Todo:
@@ -293,6 +298,47 @@ export class UserTaskItemsComponent implements OnInit, OnDestroy {
293298
});
294299
}
295300

301+
deleteTask(task: TaskItemDto): void {
302+
if (!this.canDeleteTask(task) || this.isTaskPending(task.id)) {
303+
return;
304+
}
305+
306+
this.confirmationService.confirm({
307+
header: 'Delete Task',
308+
message: `Delete task "${task.title}"? This action cannot be undone.`,
309+
icon: 'pi pi-exclamation-triangle',
310+
acceptLabel: 'Delete',
311+
rejectLabel: 'Cancel',
312+
acceptButtonStyleClass: 'p-button-danger',
313+
accept: () => {
314+
const previousTasks = [...this.tasks];
315+
this.pendingTaskIds.add(task.id);
316+
this.tasks = this.tasks.filter((entry) => entry.id !== task.id);
317+
318+
if (this.isPreviewMode) {
319+
this.pendingTaskIds.delete(task.id);
320+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted in preview mode.' });
321+
return;
322+
}
323+
324+
this.taskItemsApiClient
325+
.delete(task.id)
326+
.pipe(takeUntil(this.destroy$))
327+
.subscribe({
328+
next: () => {
329+
this.pendingTaskIds.delete(task.id);
330+
this.messageService.add({ severity: 'success', summary: 'Deleted', detail: 'Task deleted successfully.' });
331+
},
332+
error: () => {
333+
this.pendingTaskIds.delete(task.id);
334+
this.tasks = previousTasks;
335+
this.messageService.add({ severity: 'error', summary: 'Delete Failed', detail: 'Could not delete task.' });
336+
}
337+
});
338+
}
339+
});
340+
}
341+
296342
private patchTaskStatus(task: TaskItemDto, nextStatus: TaskStatus): void {
297343
const previous = task.status;
298344
task.status = nextStatus;

src/app/shared/shared.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { SkeletonModule } from 'primeng/skeleton';
2727
import { ToastModule } from 'primeng/toast';
2828
import { CalendarModule } from 'primeng/calendar';
2929
import { StepsModule } from 'primeng/steps';
30+
import { ConfirmDialogModule } from 'primeng/confirmdialog';
3031

3132
@NgModule({
3233
exports: [
@@ -62,7 +63,8 @@ import { StepsModule } from 'primeng/steps';
6263
SkeletonModule,
6364
ToastModule,
6465
CalendarModule,
65-
StepsModule
66+
StepsModule,
67+
ConfirmDialogModule
6668
],
6769
})
6870
export class SharedModule {}

0 commit comments

Comments
 (0)