Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage tasks by statuses #130

Closed
wants to merge 12 commits into from
4 changes: 2 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"styleext": "scss"
}
},
"targets": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
Expand Down Expand Up @@ -130,4 +130,4 @@
"cli": {
"analytics": false
}
}
}
11 changes: 9 additions & 2 deletions src/main/webapp/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@
<div class="logo">
<img src="assets/images/logos/coyo/logo-coyo-inversed-nav-hd.png" alt="COYO">
</div>
<tiny-task-form (created)="created($event)"></tiny-task-form>
<tiny-task-form
(created)="created()"
(clearDoneTasks)="clearFinishedTasks()"
></tiny-task-form>
</div>
</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()"
(statusChanged)="statusChanged()"
></tiny-task-list>
</div>
</section>
</div>
26 changes: 26 additions & 0 deletions src/main/webapp/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,30 @@ describe('AppComponent', () => {
expect(component.tasks$).toEqual(tasks$);
expect(taskService.getAll).toHaveBeenCalled();
});

it('should reload the tasks after finished tasks were cleared', () => {
// given
const tasks$ = of([]);
taskService.getAll.and.returnValue(tasks$);

// when
component.clearFinishedTasks();

// then
expect(component.tasks$).toEqual(tasks$);
expect(taskService.getAll).toHaveBeenCalled();
});

it('should reload the tasks after status was changed', () => {
// given
const tasks$ = of([]);
taskService.getAll.and.returnValue(tasks$);

// when
component.statusChanged();

// then
expect(component.tasks$).toEqual(tasks$);
expect(taskService.getAll).toHaveBeenCalled();
});
});
18 changes: 13 additions & 5 deletions src/main/webapp/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core';
import {Observable} from 'rxjs';

import { Task } from './tasks/task';
import { TaskService } from './tasks/task.service';
import {Task} from './tasks/task';
import {TaskService} from './tasks/task.service';

@Component({
selector: 'tiny-root',
Expand All @@ -12,7 +12,7 @@ import { TaskService } from './tasks/task.service';
})
export class AppComponent implements OnInit {

tasks$: Observable<Task[]>;
tasks$!: Observable<Task[]>;

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

Expand All @@ -27,4 +27,12 @@ export class AppComponent implements OnInit {
deleted(): void {
this.tasks$ = this.taskService.getAll();
}

statusChanged(): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know its already broken without your changes (#brokenWindow) but i would go for an "update" function which is just called in any case because every function is doing the same right now.

this.tasks$ = this.taskService.getAll();
}

clearFinishedTasks(): void {
this.tasks$ = this.taskService.getAll();
}
}
12 changes: 12 additions & 0 deletions src/main/webapp/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {NgModule} from '@angular/core';
import {TinyLetDirective} from 'app/shared/tiny-let/tiny-let.directive';

@NgModule({
exports: [
TinyLetDirective
],
declarations: [
TinyLetDirective
]
})
export class SharedModule { }
25 changes: 25 additions & 0 deletions src/main/webapp/app/shared/tiny-let/tiny-let.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Directive, Input, TemplateRef, ViewContainerRef, Inject } from '@angular/core';

export class LetContext<T> {
constructor(
private readonly directive: TinyLetDirective<T>
) { }

get tinyLet(): T | null {
return this.directive.tinyLet;
}
}

@Directive({
selector: '[tinyLet]'
})
export class TinyLetDirective<T> {
@Input() tinyLet: T | null = null;

constructor(
@Inject(ViewContainerRef) viewContainer: ViewContainerRef,
@Inject(TemplateRef) templateRef: TemplateRef<LetContext<T>>
) {
viewContainer.createEmbeddedView(templateRef, new LetContext<T>(this));
}
}
25 changes: 25 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 @@ -3,6 +3,7 @@ import { TestBed } from '@angular/core/testing';
import { BASE_URL } from 'app/app.tokens';

import { DefaultTaskService } from './default-task.service';
import {TaskStatus} from "app/tasks/task";

describe('DefaultTaskService', () => {
let httpTestingController: HttpTestingController;
Expand Down Expand Up @@ -61,4 +62,28 @@ describe('DefaultTaskService', () => {
// finally
req.flush({});
});

it('should delete a list of tasks', () => {
// when
taskService.deleteAll(['1', '2', '3']).subscribe();

// then
const req = httpTestingController.expectOne(request => request.urlWithParams === 'http://backend.tld/tasks?id=1&id=2&id=3');
expect(req.request.method).toEqual('DELETE');

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

it('should set new status for task', () => {
// when
taskService.setStatus('id123', TaskStatus.Done).subscribe();

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

// finally
req.flush({});
});
});
14 changes: 12 additions & 2 deletions src/main/webapp/app/tasks/default-task.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { HttpClient } from '@angular/common/http';
import {HttpClient, HttpParams} from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { BASE_URL } from '../app.tokens';
import { Task } from './task';
import {Task, TaskStatus} from './task';
import { TaskService } from './task.service';

@Injectable()
Expand All @@ -20,7 +20,17 @@ export class DefaultTaskService implements TaskService {
return this.http.delete<void>(this.baseUrl + '/tasks/' + id);
}

deleteAll(ids: string[]): Observable<void> {
const params = new HttpParams({fromObject: {id: ids}});
return this.http.delete<void>(this.baseUrl + '/tasks', {params});
}


getAll(): Observable<Task[]> {
return this.http.get<Task[]>(this.baseUrl + '/tasks');
}

setStatus(id: string, status: TaskStatus): Observable<Task> {
return this.http.put<Task>(this.baseUrl + '/tasks/' + id + '/status', { status });
}
}
122 changes: 89 additions & 33 deletions src/main/webapp/app/tasks/local-task.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { TestBed } from '@angular/core/testing';
import { LocalTaskService } from 'app/tasks/local-task.service';
import { Observable } from 'rxjs';
import { Task } from './task';
import {TestBed} from '@angular/core/testing';
import {LocalTaskService} from 'app/tasks/local-task.service';
import {EMPTY, Observable} from 'rxjs';
import {Task, TaskStatus} from './task';

describe('LocalTaskService', () => {
const id = 'de4f576e-d1b5-488a-8c77-63d4c8726909';
const name = 'Doing the do!';
const mockTask = `{"id":"${id}","name":"${name}"}`;

let taskService: LocalTaskService;

beforeEach(() => {
Expand All @@ -16,40 +12,100 @@ describe('LocalTaskService', () => {
});

taskService = TestBed.inject(LocalTaskService);
spyOn(localStorage, 'getItem').and.callFake(() => `[${mockTask}]`);
spyOn(localStorage, 'setItem').and.callFake(() => {});
});

it('should be created', () => {
expect(taskService).toBeTruthy();
});
describe('single task in store', () => {
beforeEach(() => {
spyOn(localStorage, 'getItem').and.callFake(() => `[${mockTask}]`);
spyOn(localStorage, 'setItem').and.callFake(() => { });
});

it('should return tasks from local storage', () => {
// when
const taskList$: Observable<Task[]> = taskService.getAll();
const id = 'de4f576e-d1b5-488a-8c77-63d4c8726909';
const name = 'Doing the do!';
const status = String(TaskStatus.Todo);
const mockTask = `{"id":"${id}","name":"${name}","status":"${status}"}`;
it('should be created', () => {
expect(taskService).toBeTruthy();
});

it('should return tasks from local storage', () => {
// when
const taskList$: Observable<Task[]> = taskService.getAll();

// then
expect(localStorage.getItem).toHaveBeenCalled();
taskList$.subscribe(taskList => {
expect(taskList.length).toBe(1);
expect(taskList[0].name).toEqual(name);
// then
expect(localStorage.getItem).toHaveBeenCalled();
taskList$.subscribe(taskList => {
expect(taskList.length).toBe(1);
expect(taskList[0].name).toEqual(name);
});
});
});

it('should write task to local storage', () => {
// when
taskService.create('Drinking the drink!');
it('should write task to local storage', () => {
// when
taskService.create('Drinking the drink!');

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

it('should delete task from local storage', () => {
// when
taskService.delete(id);

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

// then
expect(localStorage.setItem).toHaveBeenCalled();
it('should set new status for task', () => {
// when
taskService.setStatus(id, TaskStatus.Done);

// then
expect(localStorage.setItem).toHaveBeenCalledWith('tiny.tasks', JSON.stringify([
{id, name, status: TaskStatus.Done}
]));
});

it('should return updated task', () => {
// when
const result = taskService.setStatus(id, TaskStatus.Done);

// then
const subscription = result.subscribe(value => {
expect(value).toEqual({id, name, status: TaskStatus.Done});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case it could be that the expect is not even called but the test would not fail. Better use done function for tests like that

});
subscription.unsubscribe();
});

it('should return completed observable if no task was found to update', () => {
// when
const result = taskService.setStatus('id123', TaskStatus.Done);

// then
expect(result).toEqual(EMPTY);
});
});
describe('multiple tasks', () => {
beforeEach(() => {
spyOn(localStorage, 'getItem').and.callFake(() => JSON.stringify([
{ id: 'id007', name: 'bang', status: TaskStatus.Todo },
{ id: 'id123', name: 'say hello', status: TaskStatus.Todo },
{ id: 'id1234', name: 'say goodbye', status: TaskStatus.Cancelled },
{ id: 'id000', name: 'todo not todo', status: TaskStatus.Todo },
]));
spyOn(localStorage, 'setItem').and.callFake(() => { });
});

it('should delete task from local storage', () => {
// when
taskService.delete(id);
it('should delete a list of tasks from local storage', () => {
// when
taskService.deleteAll(['id123', 'id1234']);

// then
expect(localStorage.getItem).toHaveBeenCalled();
expect(localStorage.setItem).toHaveBeenCalled();
// then
expect(localStorage.setItem).toHaveBeenCalledWith('tiny.tasks', JSON.stringify([
{id: 'id007', name: 'bang', status: TaskStatus.Todo},
{id: 'id000', name: 'todo not todo', status: TaskStatus.Todo}
]));
});
});
});
Loading