From 5035b299f1025bfe24d484082e8f4d1d7d52f48a Mon Sep 17 00:00:00 2001 From: Marco Forster <marco-forster@outlook.com> Date: Thu, 28 Jul 2022 15:09:03 +0200 Subject: [PATCH] feat: extract kanban into components --- src/app/app.module.ts | 7 +- .../home/demo-board/demo-board.component.html | 1 + .../home/demo-board/demo-board.component.scss | 1 + .../home/demo-board/demo-board.component.ts | 51 +++++++++++ .../board-list/board-list.component.html | 3 + .../board-list/board-list.component.scss | 49 +---------- .../kanban/board-list/board-list.component.ts | 84 +++++++++++++------ src/app/kanban/board/board.component.scss | 43 ---------- src/app/kanban/board/board.component.ts | 62 +++++++++++--- .../kanban/dialogs/task-dialog.component.ts | 6 +- .../kanban-board/kanban-board.component.html | 19 +++++ .../kanban-board/kanban-board.component.scss | 23 +++++ .../kanban-board/kanban-board.component.ts | 62 ++++++++++++++ src/app/kanban/kanban-routing.module.ts | 4 +- src/app/kanban/kanban.module.ts | 7 +- src/app/shared/shared.module.ts | 4 +- 16 files changed, 287 insertions(+), 139 deletions(-) create mode 100644 src/app/home/demo-board/demo-board.component.html create mode 100644 src/app/home/demo-board/demo-board.component.scss create mode 100644 src/app/home/demo-board/demo-board.component.ts create mode 100644 src/app/kanban/kanban-board/kanban-board.component.html create mode 100644 src/app/kanban/kanban-board/kanban-board.component.scss create mode 100644 src/app/kanban/kanban-board/kanban-board.component.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index cf3b7d2..edc88ea 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,9 +11,11 @@ import { AngularFireModule } from '@angular/fire/compat'; import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; import { AngularFireAuthModule } from '@angular/fire/compat/auth'; import { ErrorModule } from './error/error.module'; +import { DemoBoardComponent } from './home/demo-board/demo-board.component'; +import { KanbanModule } from './kanban/kanban.module'; @NgModule({ - declarations: [AppComponent, HomeComponent], + declarations: [AppComponent, HomeComponent, DemoBoardComponent], imports: [ BrowserModule, AppRoutingModule, @@ -22,7 +24,8 @@ import { ErrorModule } from './error/error.module'; ErrorModule, AngularFireModule.initializeApp(environment.firebaseConfig), AngularFirestoreModule, - AngularFireAuthModule + AngularFireAuthModule, + KanbanModule ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/home/demo-board/demo-board.component.html b/src/app/home/demo-board/demo-board.component.html new file mode 100644 index 0000000..879c911 --- /dev/null +++ b/src/app/home/demo-board/demo-board.component.html @@ -0,0 +1 @@ +<p>It works</p> diff --git a/src/app/home/demo-board/demo-board.component.scss b/src/app/home/demo-board/demo-board.component.scss new file mode 100644 index 0000000..710cecc --- /dev/null +++ b/src/app/home/demo-board/demo-board.component.scss @@ -0,0 +1 @@ +/* Empty */ diff --git a/src/app/home/demo-board/demo-board.component.ts b/src/app/home/demo-board/demo-board.component.ts new file mode 100644 index 0000000..4a1a8bb --- /dev/null +++ b/src/app/home/demo-board/demo-board.component.ts @@ -0,0 +1,51 @@ +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { Component, OnInit } from '@angular/core'; +import { Board } from 'src/app/kanban/model/board.model'; + +@Component({ + selector: 'app-demo-board', + templateUrl: './demo-board.component.html', + styleUrls: ['./demo-board.component.scss'] +}) +export class DemoBoardComponent { + boards: Board[] = [ + { + id: '0', + title: 'Todo', + priority: 0, + tasks: [ + { + description: 'Plan my next awesome project', + label: 'yellow' + }, + { description: 'Reschedule my meeting', label: 'blue' } + ] + }, + { + id: '1', + title: 'In progress', + priority: 1, + tasks: [ + { + description: 'Plan my next awesome project', + label: 'yellow' + }, + { description: 'Reschedule my meeting', label: 'blue' } + ] + }, + { + id: '2', + title: 'Done', + priority: 2, + tasks: [ + { + description: 'Plan my next awesome project', + label: 'yellow' + }, + { description: 'Reschedule my meeting', label: 'blue' } + ] + } + ]; + + constructor() {} +} diff --git a/src/app/kanban/board-list/board-list.component.html b/src/app/kanban/board-list/board-list.component.html index 0c0a938..27c2e97 100644 --- a/src/app/kanban/board-list/board-list.component.html +++ b/src/app/kanban/board-list/board-list.component.html @@ -10,7 +10,10 @@ *ngFor="let board of boards" [board]="board" (boardExitedEvent)="saveExitedBoard($event)" + (boardDeletedEvent)="boardDelete($event)" (taskDroppedEvent)="taskDrop($event)" + (taskUpdatedEvent)="taskUpdate($event)" + (taskDeletedEvent)="taskDelete($event)" > <mat-icon cdkDragHandle class="handle">drag_indicator</mat-icon> </app-board> diff --git a/src/app/kanban/board-list/board-list.component.scss b/src/app/kanban/board-list/board-list.component.scss index 456d226..1263e65 100644 --- a/src/app/kanban/board-list/board-list.component.scss +++ b/src/app/kanban/board-list/board-list.component.scss @@ -23,15 +23,11 @@ } } -.outer-card.cdk-drag-placeholder { +.cdk-drag-placeholder { opacity: 0.2; width: 350px; border: 5px dashed gray; - margin: 0 10px; -} - -.inner-card.cdk-drag-placeholder { - opacity: 0.5; + margin: 0 12px; } .cdk-drag-animating { @@ -52,44 +48,3 @@ padding: 32px; height: 350px; } - -.outer-card { - margin: 10px; - min-width: 300px; - max-width: 300px; - padding: 10px; - background: whitesmoke; -} - -.inner-card { - margin: 5px 0; - cursor: pointer; -} - -.blue { - background: #71deff; - color: black; -} - -.green { - background: #36e9b6; - color: black; -} - -.yellow { - background: #ffcf44; - color: black; -} - -.purple { - background: #b15cff; -} - -.red { - background: #e74a4a; -} - -.gray { - background: gray; - text-decoration: line-through; -} diff --git a/src/app/kanban/board-list/board-list.component.ts b/src/app/kanban/board-list/board-list.component.ts index ef41aca..cff3e0d 100644 --- a/src/app/kanban/board-list/board-list.component.ts +++ b/src/app/kanban/board-list/board-list.component.ts @@ -3,41 +3,58 @@ import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Subscription } from 'rxjs'; import { Board } from '../model/board.model'; import { BoardService } from '../board.service'; import { MatDialog } from '@angular/material/dialog'; import { BoardDialogComponent } from '../dialogs/board-dialog.component'; +import { Task } from '../model/task.model'; @Component({ selector: 'app-board-list', templateUrl: './board-list.component.html', styleUrls: ['./board-list.component.scss'] }) -export class BoardListComponent implements OnInit, OnDestroy { - boards: Board[] = []; +export class BoardListComponent { + @Output() + boardDroppedEvent = new EventEmitter<Board[]>(); + + @Output() + boardCreatedEvent = new EventEmitter<{ title: string; priority: number }>(); + + @Output() + boardDeletedEvent = new EventEmitter<string>(); + + @Output() + taskDroppedEvent = new EventEmitter< + Array<{ + boardId: string; + tasks: Task[]; + }> + >(); + + @Output() + taskUpdatedEvent = new EventEmitter<{ + boardId: string; + tasks: Task[]; + }>(); + + @Output() + taskDeletedEvent = new EventEmitter<{ boardId: string; task: Task }>(); + + @Input() + boards?: Board[]; + previousBoard: Board = {}; dropped = true; subscription?: Subscription; constructor(public boardService: BoardService, public dialog: MatDialog) {} - ngOnInit() { - this.subscription = this.boardService - .getUserBoards() - .subscribe((boards) => { - this.boards = boards; - }); - } - - ngOnDestroy() { - this.subscription?.unsubscribe(); - } - boardDrop(event: CdkDragDrop<string[]>) { - moveItemInArray(this.boards, event.previousIndex, event.currentIndex); - this.boardService.sortBoards(this.boards); + moveItemInArray(this.boards!, event.previousIndex, event.currentIndex); + this.boardDroppedEvent.emit(this.boards); } openBoardDialog(): void { @@ -48,9 +65,10 @@ export class BoardListComponent implements OnInit, OnDestroy { dialogRef.afterClosed().subscribe((result) => { if (result) { - this.boardService.createBoard({ + this.boards?.push({ title: result, priority: this.boards?.length! }); + this.boardCreatedEvent.emit({ title: result, - priority: this.boards.length + priority: this.boards?.length! }); } }); @@ -76,7 +94,9 @@ export class BoardListComponent implements OnInit, OnDestroy { event.previousIndex, event.currentIndex ); - this.boardService.updateTasks(currentBoard.id!, currentBoard.tasks!); + this.taskDroppedEvent.emit([ + { boardId: currentBoard.id!, tasks: currentBoard.tasks! } + ]); } else { transferArrayItem( this.previousBoard.tasks!, @@ -84,12 +104,26 @@ export class BoardListComponent implements OnInit, OnDestroy { event.previousIndex, event.currentIndex ); - this.boardService.updateTasks( - this.previousBoard.id!, - this.previousBoard.tasks! - ); - this.boardService.updateTasks(currentBoard.id!, currentBoard.tasks!); + this.taskDroppedEvent.emit([ + { boardId: currentBoard.id!, tasks: currentBoard.tasks! }, + { boardId: this.previousBoard.id!, tasks: this.previousBoard.tasks! } + ]); } this.dropped = true; } + + taskUpdate({ boardId, tasks }: { boardId: string; tasks: Task[] }) { + this.taskUpdatedEvent.emit({ boardId: boardId, tasks: tasks }); + } + + boardDelete(boardId: string) { + this.boards?.forEach((board, idx) => { + if (board.id === boardId) this.boards?.splice(idx, 1); + }); + this.boardDeletedEvent.emit(boardId); + } + + taskDelete({ boardId, task }: { boardId: string; task: Task }) { + this.taskDeletedEvent.emit({ boardId: boardId, task: task }); + } } diff --git a/src/app/kanban/board/board.component.scss b/src/app/kanban/board/board.component.scss index 456d226..0056fc9 100644 --- a/src/app/kanban/board/board.component.scss +++ b/src/app/kanban/board/board.component.scss @@ -1,35 +1,3 @@ -.boards { - width: auto; - padding: 24px; - display: flex; - flex-direction: row; - overflow-x: scroll; - - &::-webkit-scrollbar { - height: 4px; - width: 4px; - } - - &::-webkit-scrollbar-thumb { - background-color: #f5f5f5; - border: 2px solid #555; - } - - .handle { - position: relative; - top: 5px; - left: 0; - cursor: move; - } -} - -.outer-card.cdk-drag-placeholder { - opacity: 0.2; - width: 350px; - border: 5px dashed gray; - margin: 0 10px; -} - .inner-card.cdk-drag-placeholder { opacity: 0.5; } @@ -42,17 +10,6 @@ transition: transform 300ms ease; } -.board-button { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border: 5px gray dashed; - width: 300px; - padding: 32px; - height: 350px; -} - .outer-card { margin: 10px; min-width: 300px; diff --git a/src/app/kanban/board/board.component.ts b/src/app/kanban/board/board.component.ts index d3e916e..366fa24 100644 --- a/src/app/kanban/board/board.component.ts +++ b/src/app/kanban/board/board.component.ts @@ -12,14 +12,29 @@ import { Task } from '../model/task.model'; styleUrls: ['./board.component.scss'] }) export class BoardComponent { - @Output() boardExitedEvent = new EventEmitter<Board>(); + @Output() + boardExitedEvent = new EventEmitter<Board>(); - @Output() taskDroppedEvent = new EventEmitter<{ + @Output() + boardDeletedEvent = new EventEmitter<string>(); + + @Output() + taskDroppedEvent = new EventEmitter<{ event: CdkDragDrop<string[]>; currentBoard: Board; }>(); - @Input() board: Board = {}; + @Output() + taskUpdatedEvent = new EventEmitter<{ + boardId: string; + tasks: Task[]; + }>(); + + @Output() + taskDeletedEvent = new EventEmitter<{ boardId: string; task: Task }>(); + + @Input() + board: Board = {}; constructor(public boardService: BoardService, public dialog: MatDialog) {} @@ -36,26 +51,47 @@ export class BoardComponent { const dialogRef = this.dialog.open(TaskDialogComponent, { width: '500px', data: task - ? { task: { ...task }, isNew: false, boardId: board?.id, idx } - : { task: newTask, isNew: true, boardId: board?.id } + ? { + task: { ...task }, + isNew: false, + toDelete: false, + boardId: board?.id, + idx + } + : { task: newTask, isNew: true, toDelete: false, boardId: board?.id } }); dialogRef.afterClosed().subscribe((result) => { if (result) { - if (result.isNew) { - this.boardService.updateTasks(board.id!, [ - ...board.tasks!, - result.task - ]); + if (result.toDelete) { + board.tasks?.forEach((task, idx) => { + if ( + task.description === result.task.description && + task.label === result.task.label + ) + this.board?.tasks?.splice(idx, 1); + }); + this.taskDeletedEvent.emit({ boardId: board.id!, task: result.task }); } else { - board.tasks?.splice(result.idx, 1, result.task); - this.boardService.updateTasks(board.id!, board.tasks!); + if (result.isNew) { + board.tasks?.push(result.task); + this.taskUpdatedEvent.emit({ + boardId: board.id!, + tasks: board.tasks! + }); + } else { + board.tasks?.splice(result.idx, 1, result.task); + this.taskUpdatedEvent.emit({ + boardId: board.id!, + tasks: board.tasks! + }); + } } } }); } handleBoardDelete(board: Board) { - this.boardService.deleteBoard(board.id!); + this.boardDeletedEvent.emit(board.id); } } diff --git a/src/app/kanban/dialogs/task-dialog.component.ts b/src/app/kanban/dialogs/task-dialog.component.ts index 01cc345..a54bfb6 100644 --- a/src/app/kanban/dialogs/task-dialog.component.ts +++ b/src/app/kanban/dialogs/task-dialog.component.ts @@ -1,6 +1,5 @@ import { Component, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { BoardService } from '../board.service'; @Component({ selector: 'app-task-dialog', @@ -43,7 +42,6 @@ export class TaskDialogComponent { constructor( public dialogRef: MatDialogRef<TaskDialogComponent>, - private boardService: BoardService, @Inject(MAT_DIALOG_DATA) public data: any ) {} @@ -52,7 +50,7 @@ export class TaskDialogComponent { } handleTaskDelete() { - this.boardService.removeTask(this.data.boardId, this.data.task); - this.dialogRef.close(); + this.data.toDelete = true; + this.dialogRef.close(this.data); } } diff --git a/src/app/kanban/kanban-board/kanban-board.component.html b/src/app/kanban/kanban-board/kanban-board.component.html new file mode 100644 index 0000000..6859297 --- /dev/null +++ b/src/app/kanban/kanban-board/kanban-board.component.html @@ -0,0 +1,19 @@ +<div class="container"> + <div class="header" *ngIf="angularFireAuth.user | async as user"> + <img class="avatar" [src]="user.photoURL" /> + <div> + <h1 class="title">Kanban Board of {{ user.displayName }}</h1> + <h2 class="subtitle">{{ user.email }}</h2> + </div> + </div> + + <app-board-list + [boards]="boards" + (boardDroppedEvent)="sortBoards($event)" + (boardCreatedEvent)="createBoard($event)" + (boardDeletedEvent)="deleteBoard($event)" + (taskDroppedEvent)="sortTasks($event)" + (taskUpdatedEvent)="updateTasks($event)" + (taskDeletedEvent)="removeTask($event)" + ></app-board-list> +</div> diff --git a/src/app/kanban/kanban-board/kanban-board.component.scss b/src/app/kanban/kanban-board/kanban-board.component.scss new file mode 100644 index 0000000..94a3710 --- /dev/null +++ b/src/app/kanban/kanban-board/kanban-board.component.scss @@ -0,0 +1,23 @@ +.container { + margin: 30px; +} + +.header { + display: flex; +} + +.avatar { + margin-right: 10px; + border-radius: 50%; + height: min-content; +} + +.title { + margin-bottom: 0; +} + +.subtitle { + margin-top: 0; + font-size: medium; + color: gray; +} diff --git a/src/app/kanban/kanban-board/kanban-board.component.ts b/src/app/kanban/kanban-board/kanban-board.component.ts new file mode 100644 index 0000000..abd9d02 --- /dev/null +++ b/src/app/kanban/kanban-board/kanban-board.component.ts @@ -0,0 +1,62 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AngularFireAuth } from '@angular/fire/compat/auth'; +import { Subscription } from 'rxjs'; +import { BoardService } from '../board.service'; +import { Board } from '../model/board.model'; +import { Task } from '../model/task.model'; + +@Component({ + selector: 'app-kanban-board', + templateUrl: './kanban-board.component.html', + styleUrls: ['./kanban-board.component.scss'] +}) +export class KanbanBoardComponent implements OnInit, OnDestroy { + boards?: Board[]; + subscription?: Subscription; + + constructor( + private boardService: BoardService, + public angularFireAuth: AngularFireAuth + ) {} + + ngOnInit(): void { + this.subscription = this.boardService + .getUserBoards() + .subscribe((boards) => { + this.boards = boards; + }); + } + + ngOnDestroy() { + this.subscription?.unsubscribe(); + } + + sortBoards(boards: Board[]) { + this.boardService.sortBoards(boards); + } + + createBoard({ title, priority }: { title: string; priority: number }) { + this.boardService.createBoard({ + title: title, + priority: priority + }); + } + + deleteBoard(boardId: string) { + this.boardService.deleteBoard(boardId); + } + + sortTasks(boardsToSort: { boardId: string; tasks: Task[] }[]) { + boardsToSort.forEach((board) => { + this.boardService.updateTasks(board.boardId, board.tasks); + }); + } + + updateTasks({ boardId, tasks }: { boardId: string; tasks: Task[] }) { + this.boardService.updateTasks(boardId, tasks); + } + + removeTask({ boardId, task }: { boardId: string; task: Task }) { + this.boardService.removeTask(boardId, task); + } +} diff --git a/src/app/kanban/kanban-routing.module.ts b/src/app/kanban/kanban-routing.module.ts index b3debe1..1e1054c 100644 --- a/src/app/kanban/kanban-routing.module.ts +++ b/src/app/kanban/kanban-routing.module.ts @@ -1,8 +1,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { BoardListComponent } from './board-list/board-list.component'; +import { KanbanBoardComponent } from './kanban-board/kanban-board.component'; -const routes: Routes = [{ path: '', component: BoardListComponent }]; +const routes: Routes = [{ path: '', component: KanbanBoardComponent }]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/src/app/kanban/kanban.module.ts b/src/app/kanban/kanban.module.ts index 038a2e9..0902ead 100644 --- a/src/app/kanban/kanban.module.ts +++ b/src/app/kanban/kanban.module.ts @@ -9,13 +9,15 @@ import { BoardListComponent } from './board-list/board-list.component'; import { BoardDialogComponent } from './dialogs/board-dialog.component'; import { TaskDialogComponent } from './dialogs/task-dialog.component'; import { BoardComponent } from './board/board.component'; +import { KanbanBoardComponent } from './kanban-board/kanban-board.component'; @NgModule({ declarations: [ BoardListComponent, BoardDialogComponent, TaskDialogComponent, - BoardComponent + BoardComponent, + KanbanBoardComponent ], imports: [ CommonModule, @@ -23,6 +25,7 @@ import { BoardComponent } from './board/board.component'; SharedModule, FormsModule, DragDropModule - ] + ], + exports: [BoardComponent] }) export class KanbanModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 583f7bb..fbcdd0f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -14,6 +14,7 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { MatGridListModule } from '@angular/material/grid-list'; import { ToolbarComponent } from './toolbar/toolbar.component'; import { FontAwesomeModule, @@ -46,7 +47,8 @@ const modules = [ MatDialogModule, MatButtonToggleModule, RouterModule, - FontAwesomeModule + FontAwesomeModule, + MatGridListModule ]; @NgModule({