refactor: remove saving of current task in local storage; adjust assignees state management

This commit is contained in:
Chneemann 2025-04-04 04:51:59 +02:00
parent eeb9f4303c
commit 7b028ab843
8 changed files with 117 additions and 170 deletions

View file

@ -34,7 +34,6 @@
id="title" id="title"
name="title" name="title"
#title="ngModel" #title="ngModel"
(input)="saveTaskData()"
placeholder="{{ placeholder="{{
taskData.title taskData.title
? taskData.title ? taskData.title
@ -61,7 +60,6 @@
name="description" name="description"
#description="ngModel" #description="ngModel"
minlength="10" minlength="10"
(input)="saveTaskData()"
placeholder="{{ placeholder="{{
taskData.description taskData.description
? taskData.description ? taskData.description
@ -79,7 +77,7 @@
</div> </div>
<app-assigned <app-assigned
[taskCreator]="taskData.creator" [taskCreator]="taskData.creator"
[assignedList]="taskData.assigned" [currentAssignees]="taskData.assignees"
(assignedChange)="receiveAssigned($event)" (assignedChange)="receiveAssigned($event)"
></app-assigned> ></app-assigned>
</div> </div>
@ -177,7 +175,6 @@
id="category" id="category"
name="category" name="category"
#category="ngModel" #category="ngModel"
(change)="saveTaskData()"
[(ngModel)]="taskData.category" [(ngModel)]="taskData.category"
required required
> >
@ -263,7 +260,7 @@
? 'block' ? 'block'
: 'none' : 'none'
}" }"
(click)="removeTaskData(taskForm)" (click)="clearTaskData(taskForm)"
></app-form-btn> ></app-form-btn>
<app-form-btn <app-form-btn
[class]="'btn-delete'" [class]="'btn-delete'"

View file

@ -54,7 +54,6 @@ export class AddTaskComponent implements OnInit {
status: this.taskService.getStatuses()[0], status: this.taskService.getStatuses()[0],
priority: this.taskService.getPriorities()[0], priority: this.taskService.getPriorities()[0],
subtasks: [], subtasks: [],
assigned: [],
assignees: [], assignees: [],
userData: [], userData: [],
creator: '', creator: '',
@ -70,9 +69,8 @@ export class AddTaskComponent implements OnInit {
*/ */
ngOnInit() { ngOnInit() {
this.setCurrentUserId(); this.setCurrentUserId();
this.loadEditTaskData(); this.loadExistingTaskData();
this.routeParams(); this.routeParams();
this.loadLocalStorageData();
} }
setCurrentUserId() { setCurrentUserId() {
@ -84,13 +82,7 @@ export class AddTaskComponent implements OnInit {
}); });
} }
/** async loadExistingTaskData() {
* Loads task data for editing if applicable, or sets the task status if this is a new task overlay.
* @param {string} overlayData The task id or status of the task to be loaded.
* @param {string} overlayType The type of overlay to be opened.
* @returns {void}
*/
async loadEditTaskData() {
if (this.overlayData) { if (this.overlayData) {
const taskData = await firstValueFrom(this.getTaskData(this.overlayData)); const taskData = await firstValueFrom(this.getTaskData(this.overlayData));
if (taskData) { if (taskData) {
@ -123,14 +115,6 @@ export class AddTaskComponent implements OnInit {
* If no task data is present in local storage, this method saves the current taskData object to local storage. * If no task data is present in local storage, this method saves the current taskData object to local storage.
* @returns {void} * @returns {void}
*/ */
loadLocalStorageData() {
const storedTaskData = localStorage.getItem('taskData');
if (storedTaskData) {
this.taskData = JSON.parse(storedTaskData);
} else {
this.saveTaskData();
}
}
/** /**
* Gets the task data from the Firebase service for the given task id. * Gets the task data from the Firebase service for the given task id.
@ -147,14 +131,12 @@ export class AddTaskComponent implements OnInit {
status: false, status: false,
}; };
this.taskData.subtasks.push(newSubtask); this.taskData.subtasks.push(newSubtask);
this.saveTaskData();
} }
deleteSubtask(subtaskToDelete: Subtask) { deleteSubtask(subtaskToDelete: Subtask) {
this.taskData.subtasks = this.taskData.subtasks.filter( this.taskData.subtasks = this.taskData.subtasks.filter(
(subtask) => subtask !== subtaskToDelete (subtask) => subtask !== subtaskToDelete
); );
this.saveTaskData();
} }
/** /**
@ -171,7 +153,9 @@ export class AddTaskComponent implements OnInit {
* @returns {void} * @returns {void}
*/ */
receiveAssigned(assigned: string[]) { receiveAssigned(assigned: string[]) {
this.taskData.assigned = assigned; this.taskData.assignees = assigned.map((userId) => ({
userId: userId,
}));
} }
/** /**
@ -203,19 +187,6 @@ export class AddTaskComponent implements OnInit {
this.taskData.priority !== priority this.taskData.priority !== priority
? (this.taskData.priority = priority) ? (this.taskData.priority = priority)
: this.taskData.priority; : this.taskData.priority;
this.saveTaskData();
}
/**
* Saves the current task data to local storage.
*
* This function first runs the cross site scripting check on the task data,
* then saves the task data to local storage.
* @returns {void}
*/
saveTaskData() {
this.checkCrossSiteScripting();
localStorage.setItem('taskData', JSON.stringify(this.taskData));
} }
/** /**
@ -223,8 +194,7 @@ export class AddTaskComponent implements OnInit {
* @param form the form to reset * @param form the form to reset
* @returns {void} * @returns {void}
*/ */
removeTaskData(form: NgForm) { clearTaskData(form: NgForm) {
localStorage.removeItem('taskData');
this.clearForm(form); this.clearForm(form);
this.clearFormData(); this.clearFormData();
} }
@ -259,7 +229,7 @@ export class AddTaskComponent implements OnInit {
clearFormData() { clearFormData() {
this.taskData.date = this.currentDate; this.taskData.date = this.currentDate;
this.taskData.category = ''; this.taskData.category = '';
this.taskData.assigned = []; this.taskData.assignees = [];
this.taskData.subtasks = []; this.taskData.subtasks = [];
} }
@ -292,7 +262,7 @@ export class AddTaskComponent implements OnInit {
next: (response) => { next: (response) => {
this.toastNotificationService.createTaskSuccessToast(); this.toastNotificationService.createTaskSuccessToast();
this.updateNotifierService.notifyUpdate('task'); this.updateNotifierService.notifyUpdate('task');
this.removeTaskData(ngForm); this.clearTaskData(ngForm);
this.closeOverlay(); this.closeOverlay();
this.router.navigate(['/board']); this.router.navigate(['/board']);
}, },
@ -327,6 +297,9 @@ export class AddTaskComponent implements OnInit {
} }
replaceXSSChars(input: string) { replaceXSSChars(input: string) {
if (!input) {
return '';
}
return input.replace(/</g, '&lt;').replace(/>/g, '&gt;'); return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
} }
} }

View file

@ -1,49 +1,35 @@
<section> <section>
@if (filteredUsers.length === 0) { @if (filteredUsers.length === 0) {
<div class="content"><p class="no-users">No users found</p></div> <div class="no-users">
} @else { @for (user of filteredUsers; track user.id) { <p>No users found</p>
<div
class="content"
(click)="addAssignedToTask(user.id)"
[ngClass]="{ selected: assigned.includes(user.id) }"
*ngIf="user.id"
>
<div
class="circle"
[ngStyle]="{
'background-color': user.color
}"
>
<div class="initials">
{{ user.initials }}
</div> </div>
} @else { @for (user of filteredUsers; track user.id) { @if (user.id) {
<div
class="user-item"
(click)="toggleAssignee(user.id)"
[ngClass]="{ selected: assignedUserIds.has(user.id) }"
>
<div class="circle" [ngStyle]="{ 'background-color': user.color }">
<div class="initials">{{ user.initials }}</div>
</div> </div>
<div class="details"> <div class="details">
<div class="name"> <div class="name">
<p> <p>{{ user.firstName }}</p>
{{ user.firstName }}
</p>
<span>,&nbsp;</span> <span>,&nbsp;</span>
<p class="last-name"> <p class="last-name">{{ user.lastName }}</p>
{{ user.lastName }}
</p>
</div> </div>
</div> </div>
<div class="checkbox"> <div class="checkbox">
@if (assigned.includes(user.id)) {
<img <img
class="checkbox-img" class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-checked.svg" [src]="
assignedUserIds.has(user.id)
? './../../../../assets/img/add-task/checkbox-checked.svg'
: './../../../../assets/img/add-task/checkbox-empty.svg'
"
alt="" alt=""
/> />
} @else {
<img
class="checkbox-img"
src="./../../../../assets/img/add-task/checkbox-empty.svg"
alt=""
/>
}
</div> </div>
</div> </div>
} } } } }
</section> </section>

View file

@ -9,19 +9,34 @@ section {
padding-top: 12px; padding-top: 12px;
border-radius: 0px 0px 20px 20px; border-radius: 0px 0px 20px 20px;
border: 1px solid var(--light-gray); border: 1px solid var(--light-gray);
border-top: 0px; border-top: 0;
background-color: var(--white); background-color: var(--white);
overflow-y: auto; overflow-y: auto;
} }
.content { .user-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 6px 12px; padding: 6px 12px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--light-gray); background-color: var(--light-gray);
} }
&.selected {
background-color: var(--dark-blue);
color: var(--white);
.checkbox-img {
filter: brightness(0) saturate(100%) invert(100%) sepia(0%)
saturate(7500%) hue-rotate(346deg) brightness(99%) contrast(103%);
}
&:hover {
background-color: var(--very-dark-blue);
}
}
} }
.circle { .circle {
@ -33,6 +48,7 @@ section {
height: 35px; height: 35px;
border-radius: 100%; border-radius: 100%;
border: 2px solid var(--white); border: 2px solid var(--white);
.initials { .initials {
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
@ -41,11 +57,6 @@ section {
} }
} }
.no-users {
font-size: 18px;
font-weight: 400;
}
.details { .details {
margin-left: 24px; margin-left: 24px;
width: 80%; width: 80%;
@ -62,28 +73,27 @@ section {
white-space: nowrap; white-space: nowrap;
font-size: 18px; font-size: 18px;
font-weight: 400; font-weight: 400;
gap: 4px;
p {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.last-name { .last-name {
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.checkbox { .checkbox-img {
img {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
}
.selected { .no-users {
background-color: var(--dark-blue); font-size: 18px;
color: var(--white); font-weight: 400;
img { padding: 8px 12px;
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(7500%)
hue-rotate(346deg) brightness(99%) contrast(103%);
}
&:hover {
background-color: var(--very-dark-blue);
}
} }

View file

@ -1,6 +1,13 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
} from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { User } from '../../../../interfaces/user.interface'; import { User } from '../../../../interfaces/user.interface';
import { Assignee } from '../../../../interfaces/task.interface';
@Component({ @Component({
selector: 'app-assigned-list', selector: 'app-assigned-list',
@ -9,79 +16,51 @@ import { User } from '../../../../interfaces/user.interface';
templateUrl: './assigned-list.component.html', templateUrl: './assigned-list.component.html',
styleUrl: './assigned-list.component.scss', styleUrl: './assigned-list.component.scss',
}) })
export class AssignedListComponent { export class AssignedListComponent implements OnChanges {
@Input() filteredUsers: User[] = []; @Input() filteredUsers: User[] = [];
@Input() searchInput: boolean = false; @Input() currentAssignees: Assignee[] = [];
@Input() taskCreator: string = '';
@Output() assignedChange = new EventEmitter<string[]>(); @Output() assignedChange = new EventEmitter<string[]>();
users: User[] = []; selectedAssignees: string[] = [];
assigned: string[] = [];
currentUser: User | null = null;
constructor() {} /**
* Retrieves a set of user IDs for the current assignees.
ngOnInit() { *
this.loadTaskAssignedData(); * @returns A Set containing the user IDs of the current assignees.
*/
get assignedUserIds(): Set<string> {
return new Set(this.currentAssignees.map((assignee) => assignee.userId));
} }
/** /**
* Updates the assignedChange event emitter with the current list of assigned users. * Syncs the selected assignees with the current assignees whenever the
* This should be called whenever the assigned list is updated. * `currentAssignees` input changes.
*/ */
updateAssigned() { ngOnChanges() {
this.assignedChange.emit(this.assigned); this.selectedAssignees = [
...this.currentAssignees.map((assignee) => assignee.userId),
];
} }
/** /**
* Toggles the assignment of a user to the task. * Emits an event with the current list of selected assignee user IDs.
*
* If the user is not currently assigned, they are added to the assigned list.
* If the user is already assigned, they are removed from the list.
* Updates the local storage with the current list of assigned users and emits the
* assignedChange event to notify other components of the update.
*
* @param userId The ID of the user to be added or removed from the assigned list.
*/ */
addAssignedToTask(userId: string) { emitAssignedChange() {
if (!this.assigned.includes(userId)) { this.assignedChange.emit(this.selectedAssignees);
this.assigned.push(userId); }
/**
* Toggles the selection status of an assignee.
*
* @param userId - The ID of the user to be toggled in the selected assignees list.
*/
toggleAssignee(userId: string) {
const index = this.selectedAssignees.indexOf(userId);
if (index === -1) {
this.selectedAssignees.push(userId);
} else { } else {
this.assigned.splice(this.assigned.indexOf(userId), 1); this.selectedAssignees.splice(index, 1);
}
this.saveTaskData();
this.updateAssigned();
}
/**
* Saves the current list of assigned users to local storage.
*
* Retrieves the current task data from local storage, updates the assigned
* list with the current list of assigned users, and saves the updated task
* data back to local storage.
*/
saveTaskData() {
let taskDataString = localStorage.getItem('taskData');
if (taskDataString !== null) {
let taskData = JSON.parse(taskDataString);
taskData.assigned = this.assigned;
localStorage.setItem('taskData', JSON.stringify(taskData));
}
}
/**
* Loads the list of assigned users from local storage.
*
* Retrieves the task data from local storage, checks if the assigned list exists,
* and if so, assigns the list to the local assigned variable.
*/
loadTaskAssignedData() {
const taskDataString = localStorage.getItem('taskData');
if (taskDataString !== null) {
const taskData = JSON.parse(taskDataString);
if (taskData.hasOwnProperty('assigned')) {
this.assigned = taskData.assigned;
}
} }
this.emitAssignedChange();
} }
} }

View file

@ -15,8 +15,7 @@
@if (showAssignedList) { @if (showAssignedList) {
<app-assigned-list <app-assigned-list
[filteredUsers]="filteredUsers" [filteredUsers]="filteredUsers"
[taskCreator]="taskCreator" [currentAssignees]="currentAssignees"
[searchInput]="searchInput"
(assignedChange)="receiveAssigned($event)" (assignedChange)="receiveAssigned($event)"
> >
></app-assigned-list ></app-assigned-list
@ -31,21 +30,19 @@
} }
<div class="assigned-badge"> <div class="assigned-badge">
@for (user of users; track user; let index = $index) { @for (users of @for (user of users; track user) { @if (user.id &&
assignedList; track users ) { @if (user.id === users) { assignedUserIds.has(user.id || '')) {
<div <div
class="circle" class="circle"
(mousemove)="openTooltipUser(user.id, $event)" (mousemove)="openTooltipUser(user.id, $event)"
(mouseleave)="closeTooltipUser()" (mouseleave)="closeTooltipUser()"
[ngStyle]="{ [ngStyle]="{ 'background-color': user.color }"
'background-color': user.color
}"
> >
<div class="initials"> <div class="initials">
{{ user.initials }} {{ user.initials }}
</div> </div>
</div> </div>
} } } @if (tooltipUserId) { } } @if (tooltipUserId) {
<div <div
class="tooltip-user" class="tooltip-user"
[style.left.px]="dialogX" [style.left.px]="dialogX"

View file

@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms';
import { User } from '../../../interfaces/user.interface'; import { User } from '../../../interfaces/user.interface';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '../../../services/api.service';
import { map, catchError, of } from 'rxjs'; import { map, catchError, of } from 'rxjs';
import { Assignee } from '../../../interfaces/task.interface';
@Component({ @Component({
selector: 'app-assigned', selector: 'app-assigned',
@ -22,14 +23,14 @@ import { map, catchError, of } from 'rxjs';
}) })
export class AssignedComponent { export class AssignedComponent {
@Input() taskCreator: string = ''; @Input() taskCreator: string = '';
@Input() assignedList: string[] = []; @Input() currentAssignees: Assignee[] = [];
@Output() assignedChange = new EventEmitter<string[]>(); @Output() assignedChange = new EventEmitter<string[]>();
tooltipUserId: string | null = null; tooltipUserId: string | null = null;
filteredUsers: User[] = []; filteredUsers: User[] = [];
users: User[] = []; users: User[] = [];
searchValue: string = ''; searchValue: string = '';
searchInput: boolean = false; showSearch: boolean = false;
showAssignedList: boolean = false; showAssignedList: boolean = false;
dialogX: number = 0; dialogX: number = 0;
dialogY: number = 0; dialogY: number = 0;
@ -55,7 +56,7 @@ export class AssignedComponent {
searchTask(): void { searchTask(): void {
this.searchValue = this.replaceXSSChars(this.searchValue) || ''; this.searchValue = this.replaceXSSChars(this.searchValue) || '';
this.searchInput = this.searchValue.trim().length > 0; this.showSearch = this.searchValue.trim().length > 0;
const searchValue = this.searchValue.toLowerCase(); const searchValue = this.searchValue.toLowerCase();
this.filteredUsers = this.users.filter( this.filteredUsers = this.users.filter(
@ -95,6 +96,10 @@ export class AssignedComponent {
this.showAssignedList = !this.showAssignedList; this.showAssignedList = !this.showAssignedList;
} }
get assignedUserIds(): Set<string> {
return new Set(this.currentAssignees.map((assignee) => assignee.userId));
}
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
checkOpenNavbar(event: MouseEvent) { checkOpenNavbar(event: MouseEvent) {
const targetElement = event.target as HTMLElement; const targetElement = event.target as HTMLElement;

View file

@ -8,7 +8,6 @@ export interface Task {
status: string; status: string;
priority: string; priority: string;
subtasks: Subtask[]; subtasks: Subtask[];
assigned: string[];
assignees: Assignee[]; assignees: Assignee[];
userData: UserSummary[]; userData: UserSummary[];
creator: string; creator: string;
@ -22,5 +21,6 @@ export interface Subtask {
} }
export interface Assignee { export interface Assignee {
id?: string;
userId: string; userId: string;
} }