Skip to content

Commit

Permalink
Finish task mindsmash#29 and update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
VictoriaWika committed Apr 25, 2021
1 parent 5244d5c commit b8fc2a5
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 14 deletions.
8 changes: 7 additions & 1 deletion src/main/webapp/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
</mat-toolbar>
<section class="content">
<div class="wrapper">
<tiny-task-list [tasks]="tasks$ | async" (deleted)="deleted($event)"></tiny-task-list>
<tiny-task-list
[tasks]="tasks$ | async"
(deleted)="deleted($event)"
(togglecompleted)="togglecompleted()"
(clearcompleted)="clearcompleted()"
>
</tiny-task-list>
</div>
</section>
</div>
8 changes: 8 additions & 0 deletions src/main/webapp/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ export class AppComponent implements OnInit {
deleted(): void {
this.tasks$ = this.taskService.getAll();
}

togglecompleted(): void {
this.tasks$ = this.taskService.getAll();
}

clearcompleted(): void {
this.tasks$ = this.taskService.getAll();
}
}
24 changes: 24 additions & 0 deletions src/main/webapp/app/tasks/default-task.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,28 @@ describe('DefaultTaskService', () => {
// finally
req.flush({});
});

it('should update completed status of a task', () => {
// when
taskService.togglecomplete('id123').subscribe();

// then
const req = httpTestingController.expectOne(request => request.url === 'http://backend.tld/tasks/id123/completed');
expect(req.request.method).toEqual('PUT');

// finally
req.flush({});
});

it('should delete all tasks', () => {
// when
taskService.clearcomplete().subscribe();

// then
const req = httpTestingController.expectOne(request => request.url === 'http://backend.tld/tasks/completed');
expect(req.request.method).toEqual('DELETE');

// finally
req.flush({});
});
});
8 changes: 8 additions & 0 deletions src/main/webapp/app/tasks/default-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export class DefaultTaskService implements TaskService {
return this.http.delete<void>(this.baseUrl + '/tasks/' + id);
}

togglecomplete(id:string): Observable<void> {
return this.http.put<void>(this.baseUrl + '/tasks/' + id + '/completed', {id} as Task);
}

clearcomplete(): Observable<void> {
return this.http.delete<void>(this.baseUrl + '/tasks/completed');
}

getAll(): Observable<Task[]> {
return this.http.get<Task[]>(this.baseUrl + '/tasks');
}
Expand Down
21 changes: 20 additions & 1 deletion src/main/webapp/app/tasks/local-task.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Task } from './task';
describe('LocalTaskService', () => {
const id = 'de4f576e-d1b5-488a-8c77-63d4c8726909';
const name = 'Doing the do!';
const mockTask = `{"id":"${id}","name":"${name}"}`;
const completed = false;
const mockTask = `{"id":"${id}","name":"${name}", "completed":"${completed}"}`;

let taskService: LocalTaskService;

Expand Down Expand Up @@ -52,4 +53,22 @@ describe('LocalTaskService', () => {
expect(localStorage.getItem).toHaveBeenCalled();
expect(localStorage.setItem).toHaveBeenCalled();
});

it('should update the completed status of a task in local storage', () => {
// when
taskService.togglecomplete(id);

// then
expect(localStorage.getItem).toHaveBeenCalled();
expect(localStorage.setItem).toHaveBeenCalled();
});

it('should delete all completed tasks from local storage', () => {
// when
taskService.clearcomplete();

// then
expect(localStorage.getItem).toHaveBeenCalled();
expect(localStorage.setItem).toHaveBeenCalled();
});
});
21 changes: 19 additions & 2 deletions src/main/webapp/app/tasks/local-task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export class LocalTaskService implements TaskService {
private static readonly STORAGE_KEY: string = 'tiny.tasks';

getAll(): Observable<Task[]> {
return of(this.readTasks());
return of(this.readTasks().sort((x,y) => x.completed === y.completed ? 0 : x? 1 : -1));
}

create(name: string): Observable<Task> {
const tasks = this.readTasks();
const task = {id: uuid(), name};
const task = {id: uuid(), name, completed: false};
tasks.push(task);
this.writeTasks(tasks);
return of(task);
Expand All @@ -32,6 +32,23 @@ export class LocalTaskService implements TaskService {
return of(null);
}

togglecomplete(id:string): Observable<void> {
const tasks = this.readTasks();
tasks.map(task => {
if ( task.id === id) {
task.completed = !task.completed;
this.writeTasks(tasks);
}
})
return of(null);
}

clearcomplete(): Observable<void> {
const tasks = this.readTasks();
this.writeTasks(tasks.filter(task => task.completed === false));
return of(null);
}

private readTasks(): Task[] {
const tasks = localStorage.getItem(LocalTaskService.STORAGE_KEY);
return tasks ? JSON.parse(tasks) : [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('TaskFormComponent', () => {
it('should create a task', () => {
// given
component.taskForm.setValue({name: 'My task'});
taskService.create.and.returnValue(of({id: 'id', name: 'My task'}));
taskService.create.and.returnValue(of({id: 'id', name: 'My task', completed: false}));

// when
component.onSubmit();
Expand All @@ -52,20 +52,20 @@ describe('TaskFormComponent', () => {
it('should emit the task after creation', () => {
// given
component.taskForm.setValue({name: 'My task'});
taskService.create.and.returnValue(of({id: 'id', name: 'My task'}));
taskService.create.and.returnValue(of({id: 'id', name: 'My task', completed: false}));
const createEmitter = spyOn(component.created, 'emit');

// when
component.onSubmit();

// then
expect(createEmitter).toHaveBeenCalledWith({id: 'id', name: 'My task'});
expect(createEmitter).toHaveBeenCalledWith({id: 'id', name: 'My task', completed: false});
});

it('should reset the form after creation', () => {
// given
component.taskForm.setValue({name: 'My task'});
taskService.create.and.returnValue(of({id: 'id', name: 'My task'}));
taskService.create.and.returnValue(of({id: 'id', name: 'My task', completed: false}));
const formReset = spyOn(component.taskForm, 'reset');

// when
Expand Down
4 changes: 3 additions & 1 deletion src/main/webapp/app/tasks/task-list/task-list.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<mat-list data-cy="task-list">
<mat-list-item *ngFor="let task of tasks" class="mat-elevation-z1">
<mat-icon mat-list-icon>assignment</mat-icon>
<h4 mat-line>{{task.name}}</h4>
<mat-checkbox [checked]="task.completed" (change)="togglecomplete(task)" class="mat-list-checkbox" color="primary"></mat-checkbox>
<h4 mat-line [class.completed]="task.completed">{{task.name}}</h4>
<button mat-icon-button aria-label="Delete task" color="primary">
<mat-icon aria-label="Delete task" (click)="delete(task)">delete</mat-icon>
</button>
</mat-list-item>
<button mat-raised-button (click)="clearcomplete()">Clear completed tasks</button>
</mat-list>
19 changes: 19 additions & 0 deletions src/main/webapp/app/tasks/task-list/task-list.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.mat-list > button {
display: block;
margin: 0 auto;
padding: 5px 20px;
}

.mat-list-item {
background: #fff;
margin: 16px 0;
Expand All @@ -8,6 +14,19 @@
user-select: none;
}

.mat-list-checkbox {
display: flex;
justify-content: center;
align-items: center;
width: 40px;
height: 40px;
}

.completed {
text-decoration: line-through;
color: #c8c8c8;
}

button .mat-icon {
color: #101d30;
}
Expand Down
28 changes: 25 additions & 3 deletions src/main/webapp/app/tasks/task-list/task-list.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('TaskListComponent', () => {
taskService.delete.and.returnValue(of(null));

// when
component.delete({id: 'id', name: 'My task'});
component.delete({id: 'id', name: 'My task', completed: false});

// then
expect(taskService.delete).toHaveBeenCalledWith('id');
Expand All @@ -48,9 +48,31 @@ describe('TaskListComponent', () => {
const deleteEmitter = spyOn(component.deleted, 'emit');

// when
component.delete({id: 'id', name: 'My task'});
component.delete({id: 'id', name: 'My task', completed: false});

// then
expect(deleteEmitter).toHaveBeenCalledWith({id: 'id', name: 'My task'});
expect(deleteEmitter).toHaveBeenCalledWith({id: 'id', name: 'My task', completed: false});
});

it('should mark a task as completed', () => {
// given
taskService.togglecomplete.and.returnValue(of(null));

// when
component.togglecomplete({id: 'id', name: 'My task', completed: false});

// then
expect(taskService.togglecomplete).toHaveBeenCalledWith('id');
});

it('should delete all completed tasks', () => {
// given
taskService.clearcomplete.and.returnValue(of(null));

// when
component.clearcomplete();

// then
expect(taskService.clearcomplete).toHaveBeenCalled();
});
});
14 changes: 14 additions & 0 deletions src/main/webapp/app/tasks/task-list/task-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,25 @@ export class TaskListComponent {

@Output() deleted: EventEmitter<Task> = new EventEmitter();

@Output() togglecompleted: EventEmitter<Task> = new EventEmitter();

@Output() clearcompleted: EventEmitter<Task> = new EventEmitter();

constructor(@Inject('TaskService') private taskService: TaskService) { }

delete(task: Task): void {
this.taskService.delete(task.id).subscribe(() => {
this.deleted.emit(task);
});
}
togglecomplete(task: Task) {
this.taskService.togglecomplete(task.id).subscribe(() => {
this.togglecompleted.emit(task);
});
}
clearcomplete() {
this.taskService.clearcomplete().subscribe(() => {
this.clearcompleted.emit();
});
}
}
15 changes: 15 additions & 0 deletions src/main/webapp/app/tasks/task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,19 @@ export interface TaskService {
* @returns an empty `Observable`
*/
delete(id: string): Observable<void>;

/**
* Toggles the completion status of a task with the given ID from the list of tasks.
*
* @param id the ID of the task to be removed
* @returns an empty `Observable`
*/
togglecomplete(id: string): Observable<void>;

/**
* Clears the completed tasks from the list of tasks.
*
* @returns an empty `Observable`
*/
clearcomplete(): Observable<void>;
}
1 change: 1 addition & 0 deletions src/main/webapp/app/tasks/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
export interface Task {
id: string;
name: string;
completed: boolean;
}
6 changes: 4 additions & 2 deletions src/main/webapp/app/tasks/tasks.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';

import { TaskFormComponent } from './task-form/task-form.component';
import { TaskListComponent } from './task-list/task-list.component';


@NgModule({
declarations: [TaskFormComponent, TaskListComponent],
imports: [
Expand All @@ -17,7 +18,8 @@ import { TaskListComponent } from './task-list/task-list.component';
MatButtonModule,
MatIconModule,
MatInputModule,
MatListModule
MatListModule,
MatCheckboxModule
],
exports: [TaskFormComponent, TaskListComponent]
})
Expand Down

0 comments on commit b8fc2a5

Please sign in to comment.