feat: integrate moveTask functionality into TaskMenuComponent

This commit is contained in:
Chneemann 2025-04-13 13:49:35 +02:00
parent 2632f859fe
commit 00b811afbd
7 changed files with 129 additions and 51 deletions

View file

@ -57,7 +57,10 @@
<div id="todo" class="details"> <div id="todo" class="details">
<!-- Loop through 'todo' tasks and display them --> <!-- Loop through 'todo' tasks and display them -->
@for (task of filteredTasks[TODO]; track task.id) { @for (task of filteredTasks[TODO]; track task.id) {
<app-task [task]="task"></app-task> <app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'todo' tasks exist --> <!-- Show empty state if no 'todo' tasks exist -->
} @empty { } @empty {
@ -90,7 +93,10 @@
<div id="inprogress" class="details"> <div id="inprogress" class="details">
<!-- Loop through 'in progress' tasks and display them --> <!-- Loop through 'in progress' tasks and display them -->
@for (task of filteredTasks[IN_PROGRESS]; track task.id) { @for (task of filteredTasks[IN_PROGRESS]; track task.id) {
<app-task [task]="task"></app-task> <app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'in progress' tasks exist --> <!-- Show empty state if no 'in progress' tasks exist -->
} @empty { } @empty {
@ -123,7 +129,10 @@
<div id="awaitfeedback" class="details"> <div id="awaitfeedback" class="details">
<!-- Loop through 'await feedback' tasks and display them --> <!-- Loop through 'await feedback' tasks and display them -->
@for (task of filteredTasks[AWAIT_FEEDBACK]; track task.id) { @for (task of filteredTasks[AWAIT_FEEDBACK]; track task.id) {
<app-task [task]="task"></app-task> <app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'await feedback' tasks exist --> <!-- Show empty state if no 'await feedback' tasks exist -->
} @empty { } @empty {
@ -157,7 +166,10 @@
<div id="done" class="details"> <div id="done" class="details">
<!-- Loop through 'done' tasks and display them --> <!-- Loop through 'done' tasks and display them -->
@for (task of filteredTasks[DONE]; track task.id) { @for (task of filteredTasks[DONE]; track task.id) {
<app-task [task]="task"></app-task> <app-task
[task]="task"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task>
<!-- Show empty state if no 'done' tasks exist --> <!-- Show empty state if no 'done' tasks exist -->
} @empty { } @empty {

View file

@ -9,7 +9,7 @@ import { Router } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component'; import { TaskHighlightedComponent } from './task/task-highlighted/task-highlighted.component';
import { ApiService } from '../../services/api.service'; import { ApiService } from '../../services/api.service';
import { Task } from '../../interfaces/task.interface'; import { Task, TaskMoveEvent } from '../../interfaces/task.interface';
import { TaskService } from '../../services/task.service'; import { TaskService } from '../../services/task.service';
import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component'; import { LoadingSpinnerComponent } from '../../shared/components/loading-spinner/loading-spinner.component';
import { debounceTime, finalize, Subject, take, takeUntil } from 'rxjs'; import { debounceTime, finalize, Subject, take, takeUntil } from 'rxjs';
@ -38,13 +38,13 @@ export class BoardComponent implements OnInit, OnDestroy {
readonly AWAIT_FEEDBACK = 'awaitfeedback'; readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done'; readonly DONE = 'done';
allTasks: Task[] = []; allTasks: Array<Task> = [];
filteredTasks: { [key: string]: Task[] } = {}; filteredTasks: { [key: string]: Task[] } = {};
searchValue: string = ''; taskMovedTo = '';
searchInput: boolean = false; taskMovedFrom = '';
taskMovedTo: string = ''; searchValue = '';
taskMovedFrom: string = ''; searchInput = false;
isLoading = false; isLoading = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -151,6 +151,14 @@ export class BoardComponent implements OnInit, OnDestroy {
}); });
} }
/**
* Updates the status of a task when its status is changed.
* @param event The event containing the task and the new status to move to.
*/
onStatusUpdate(event: TaskMoveEvent) {
this.handleItemDropped(event.task, event.moveTo);
}
/** /**
* Handles the event when an item is dropped on a column. * Handles the event when an item is dropped on a column.
* @param taskId The id of the task being moved. * @param taskId The id of the task being moved.

View file

@ -1,11 +1,11 @@
<section> <section>
@if (boardTaskStatus !== 'todo') { @if (boardTaskStatus !== TODO) {
<div class="link" (click)="moveTask('todo')">To do</div> <div class="link" (click)="moveTask(TODO)">To do</div>
} @if (boardTaskStatus !== 'inprogress') { } @if (boardTaskStatus !== IN_PROGRESS) {
<div class="link" (click)="moveTask('inprogress')">In progress</div> <div class="link" (click)="moveTask(IN_PROGRESS)">In progress</div>
} @if (boardTaskStatus !== 'awaitfeedback') { } @if (boardTaskStatus !== AWAIT_FEEDBACK) {
<div class="link" (click)="moveTask('awaitfeedback')">Await feedback</div> <div class="link" (click)="moveTask(AWAIT_FEEDBACK)">Await feedback</div>
} @if (boardTaskStatus !== 'done') { } @if (boardTaskStatus !== DONE) {
<div class="link" (click)="moveTask('done')">Done</div> <div class="link" (click)="moveTask(DONE)">Done</div>
} }
</section> </section>

View file

@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Task, TaskMoveEvent } from '../../../../interfaces/task.interface';
@Component({ @Component({
selector: 'app-task-menu', selector: 'app-task-menu',
@ -8,22 +9,19 @@ import { Component, Input } from '@angular/core';
styleUrl: './task-menu.component.scss', styleUrl: './task-menu.component.scss',
}) })
export class TaskMenuComponent { export class TaskMenuComponent {
@Input() taskId: string = ''; @Input() task!: Task;
@Input() boardTaskStatus: string = ''; @Input() boardTaskStatus!: string;
@Output() updateStatusEmitter = new EventEmitter<TaskMoveEvent>();
constructor() {} readonly TODO = 'todo';
readonly IN_PROGRESS = 'inprogress';
readonly AWAIT_FEEDBACK = 'awaitfeedback';
readonly DONE = 'done';
/**
* Moves the task to a specified status.
*
* This function updates the status of the task identified by `taskId`
* in both `allTasks` and `filteredTasks` lists to the new status
* specified by `moveTo`. It also updates the task status in the
* Firestore database.
*
* @param moveTo - The new status to move the task to.
*/
moveTask(moveTo: string) { moveTask(moveTo: string) {
// TODO this.updateStatusEmitter.emit({
task: this.task,
moveTo: moveTo,
});
} }
} }

View file

@ -1,10 +1,13 @@
<section> <section>
@if (task.id) { @if (task.id) {
<div (click)="handleMenuButtonClick($event, task.id)"> <div (click)="handleMenuButtonClick($event, task.id)">
{{ disableDrag }}
<div <div
class="content" class="content"
draggable="true" [draggable]="disableDrag"
(dragstart)="dragDropService.startDragging($event, task)" (dragstart)="
disableDrag ? dragDropService.startDragging($event, task) : null
"
> >
<div class="header"> <div class="header">
<div <div
@ -65,10 +68,11 @@
</div> </div>
</div> </div>
</div> </div>
@if (isMenuOpen){ @if (menuOpen){
<app-task-menu <app-task-menu
[taskId]="task.id" [task]="task"
[boardTaskStatus]="task.status" [boardTaskStatus]="task.status"
(updateStatusEmitter)="onStatusUpdate($event)"
></app-task-menu> ></app-task-menu>
} @if (AssignedDialogId != '') { } @if (AssignedDialogId != '') {
<div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY"> <div class="dialog" [style.left.px]="dialogX" [style.top.px]="dialogY">

View file

@ -1,7 +1,17 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, HostListener, Input } from '@angular/core'; import {
Component,
EventEmitter,
HostListener,
Input,
Output,
} from '@angular/core';
import { DragDropService } from '../../../services/drag-drop.service'; import { DragDropService } from '../../../services/drag-drop.service';
import { Task } from '../../../interfaces/task.interface'; import {
Assignee,
Task,
TaskMoveEvent,
} from '../../../interfaces/task.interface';
import { OverlayService } from '../../../services/overlay.service'; import { OverlayService } from '../../../services/overlay.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TaskMenuComponent } from './task-menu/task-menu.component'; import { TaskMenuComponent } from './task-menu/task-menu.component';
@ -16,15 +26,13 @@ import { take } from 'rxjs';
styleUrl: './task.component.scss', styleUrl: './task.component.scss',
}) })
export class TaskComponent { export class TaskComponent {
@Input() task: Task = {} as Task; @Input() task!: Task;
@Output() updateStatusEmitter = new EventEmitter<TaskMoveEvent>();
isMenuOpen: boolean = false; readonly DISABLE_DRAG_BREAKPOINT = 667;
AssignedDialogId: string = ''; readonly DIALOG_OFFSET_X = 25;
dialogX: number = 0; readonly DIALOG_OFFSET_Y = 10;
dialogY: number = 0;
creator: any = '';
assignees: any[] = [];
categoryColors = new Map<string, string>([ categoryColors = new Map<string, string>([
['User Story', '#0038ff'], ['User Story', '#0038ff'],
['Technical Task', '#20d7c2'], ['Technical Task', '#20d7c2'],
@ -32,6 +40,15 @@ export class TaskComponent {
pageViewMedia$ = this.resizeService.pageViewMedia$; pageViewMedia$ = this.resizeService.pageViewMedia$;
menuOpen = false;
disableDrag = false;
AssignedDialogId = '';
dialogX = 0;
dialogY = 0;
creator = '';
assignees: Assignee[] = [];
constructor( constructor(
public dragDropService: DragDropService, public dragDropService: DragDropService,
public overlayService: OverlayService, public overlayService: OverlayService,
@ -39,6 +56,20 @@ export class TaskComponent {
private router: Router private router: Router
) {} ) {}
/**
* Sets the initial state of the `disableDrag` property based on the window width.
*/
ngOnInit(): void {
this.updateDisableDragStatus();
}
/**
* Updates the `disableDrag` property based on the current window width.
*/
private updateDisableDragStatus(): void {
this.disableDrag = window.innerWidth > this.DISABLE_DRAG_BREAKPOINT;
}
/** /**
* Handles the button click event on a task in the board * Handles the button click event on a task in the board
* @param event the MouseEvent * @param event the MouseEvent
@ -60,7 +91,7 @@ export class TaskComponent {
* Toggle the task menu for the current task * Toggle the task menu for the current task
*/ */
toggleTaskMenu() { toggleTaskMenu() {
this.isMenuOpen = !this.isMenuOpen; this.menuOpen = !this.menuOpen;
} }
/** /**
@ -72,7 +103,7 @@ export class TaskComponent {
* If the page is not in the media view, open the task overlay * If the page is not in the media view, open the task overlay
*/ */
openTaskDetailsOverlay(taskId: string) { openTaskDetailsOverlay(taskId: string) {
if (this.isMenuOpen) { if (this.menuOpen) {
this.toggleTaskMenu(); this.toggleTaskMenu();
} }
this.pageViewMedia$.pipe(take(1)).subscribe((isPageViewMedia) => { this.pageViewMedia$.pipe(take(1)).subscribe((isPageViewMedia) => {
@ -96,7 +127,7 @@ export class TaskComponent {
); );
if (!isMenuClicked) { if (!isMenuClicked) {
this.isMenuOpen = false; this.menuOpen = false;
} }
} }
@ -117,8 +148,8 @@ export class TaskComponent {
* @param event the MouseEvent that triggered the dialog * @param event the MouseEvent that triggered the dialog
*/ */
updateDialogPosition(event: MouseEvent) { updateDialogPosition(event: MouseEvent) {
this.dialogX = event.clientX + 25; this.dialogX = event.clientX + this.DIALOG_OFFSET_X;
this.dialogY = event.clientY + 10; this.dialogY = event.clientY + this.DIALOG_OFFSET_Y;
} }
/** /**
@ -149,4 +180,24 @@ export class TaskComponent {
return (completedSubtasksCount / this.task.subtasks.length) * 100; return (completedSubtasksCount / this.task.subtasks.length) * 100;
} }
/**
* Emits an event to update the status of a task.
* @param event The TaskMoveEvent containing the task and the new status to move to.
*/
onStatusUpdate(event: TaskMoveEvent) {
this.updateStatusEmitter.emit({
task: event.task,
moveTo: event.moveTo,
});
}
@HostListener('window:resize')
/**
* Called when the window is resized.
* Updates the disableDragStatus so that tasks are not draggable on mobile devices.
*/
onResize() {
this.updateDisableDragStatus();
}
} }

View file

@ -24,3 +24,8 @@ export interface Assignee {
id?: string; id?: string;
userId: string; userId: string;
} }
export interface TaskMoveEvent {
task: Task;
moveTo: string;
}