Skip to content

Commit d4c42b0

Browse files
committed
feat: #569 create a user store and consume the service of users
1 parent 5c0497c commit d4c42b0

File tree

15 files changed

+330
-22
lines changed

15 files changed

+330
-22
lines changed

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { ProjectTypeListComponent } from './modules/customer-management/componen
5555
// tslint:disable-next-line: max-line-length
5656
import { CreateProjectTypeComponent } from './modules/customer-management/components/projects-type/components/create-project-type/create-project-type.component';
5757
import { CustomerEffects } from './modules/customer-management/store/customer-management.effects';
58+
import { UserEffects } from './modules/users/store/user.effects';
5859
import { EntryEffects } from './modules/time-clock/store/entry.effects';
5960
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
6061
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
@@ -148,6 +149,7 @@ const maskConfig: Partial<IConfig> = {
148149
TechnologyEffects,
149150
ProjectTypeEffects,
150151
EntryEffects,
152+
UserEffects,
151153
]),
152154
ToastrModule.forRoot()
153155
],

src/app/modules/users/components/users-list/users-list.component.html

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<table class="table table-sm table-bordered table-striped mb-0" datatable>
1+
<table *ngIf="users" class="table table-sm table-bordered table-striped mb-0" datatable [dtTrigger]="dtTrigger">
22
<thead class="thead-blue">
33
<tr class="d-flex">
44
<th class="col-3">User Email</th>
@@ -7,18 +7,15 @@
77
<th class="col-3 text-center">Options</th>
88
</tr>
99
</thead>
10-
<tbody>
11-
<tr class="d-flex">
12-
<td class="col-sm-3">[email protected]</td>
13-
<td class="col-sm-3">Paul Rocha</td>
14-
<td class="col-sm-3">Intern</td>
15-
<td class="col-sm-3 text-center">
16-
<button type="button" class="btn btn-sm btn-primary">
17-
<i class="fa fa-pencil fa-xs"></i>
18-
</button>
19-
<button type="button" class="btn btn-sm btn-danger ml-2">
20-
<i class="fa fa-trash-alt fa-xs"></i>
21-
</button>
10+
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
11+
<tbody *ngIf="(isLoading$ | async) === false">
12+
<tr class="d-flex" *ngFor="let user of users">
13+
<td class="col-sm-3">{{ user.email }}</td>
14+
<td class="col-sm-3">{{ user.name }}</td>
15+
<td class="col-sm-3">{{ user.role }}</td>
16+
<td class="col-sm-3 text-center custom-control custom-switch">
17+
<input type="checkbox" class="custom-control-input" id="{{user.name}}" [(ngModel)]="user.role" (click)="changeSwitch()"/>
18+
<label class="custom-control-label" for="{{user.name}}"></label>
2219
</td>
2320
</tr>
2421
</tbody>
Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,81 @@
11
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { MockStore, provideMockStore } from '@ngrx/store/testing';
23

4+
import { NgxPaginationModule } from 'ngx-pagination';
35
import { UsersListComponent } from './users-list.component';
6+
import { UserActionTypes, UserState, LoadUsers } from '../../store';
7+
import { ActionsSubject } from '@ngrx/store';
8+
import { DataTablesModule } from 'angular-datatables';
9+
import { act } from '@ngrx/effects';
410

511
describe('UsersListComponent', () => {
612
let component: UsersListComponent;
713
let fixture: ComponentFixture<UsersListComponent>;
14+
let store: MockStore<UserState>;
15+
const actionSub: ActionsSubject = new ActionsSubject();
16+
17+
const state = {
18+
data: [{ name: 'name', email: 'email', role: 'role', id: 'id', tenand_id: 'tenand id', deleted: 'delete' }],
19+
isLoading: false,
20+
message: '',
21+
};
822

923
beforeEach(async(() => {
1024
TestBed.configureTestingModule({
11-
declarations: [ UsersListComponent ]
12-
})
13-
.compileComponents();
25+
imports: [NgxPaginationModule, DataTablesModule],
26+
declarations: [UsersListComponent],
27+
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
28+
}).compileComponents();
1429
}));
1530

1631
beforeEach(() => {
1732
fixture = TestBed.createComponent(UsersListComponent);
1833
component = fixture.componentInstance;
34+
store = TestBed.inject(MockStore);
35+
store.setState(state);
1936
fixture.detectChanges();
2037
});
2138

2239
it('should create', () => {
2340
expect(component).toBeTruthy();
2441
});
42+
43+
it('when the component is initialized the load User action is triggered', () => {
44+
spyOn(store, 'dispatch');
45+
46+
component.ngOnInit();
47+
48+
expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
49+
});
50+
51+
it('on success load users, the user list should be populated', () => {
52+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
53+
const action = {
54+
type: UserActionTypes.LOAD_USERS_SUCCESS,
55+
payload: state.data,
56+
};
57+
58+
actionSubject.next(action);
59+
60+
expect(component.users).toEqual(state.data);
61+
});
62+
63+
it('on success load users, the datatable should be reloaded', async () => {
64+
const actionSubject = TestBed.inject(ActionsSubject);
65+
const action = {
66+
type: UserActionTypes.LOAD_USERS_SUCCESS,
67+
payload: state.data,
68+
};
69+
spyOn(component.dtElement.dtInstance, 'then');
70+
71+
actionSubject.next(action);
72+
73+
expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
74+
});
75+
76+
afterEach(() => {
77+
component.dtTrigger.unsubscribe();
78+
component.loadUsersSubscription.unsubscribe();
79+
fixture.destroy();
80+
});
2581
});
Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,59 @@
1-
import { Component, OnInit } from '@angular/core';
2-
1+
import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
2+
import { ActionsSubject, select, Store } from '@ngrx/store';
3+
import { DataTableDirective } from 'angular-datatables';
4+
import { Observable, Subject, Subscription } from 'rxjs';
5+
import { delay, filter } from 'rxjs/operators';
6+
import { User } from '../../models/users';
7+
import { LoadUsers, UserActionTypes } from '../../store/user.actions';
8+
import { getIsLoading } from '../../store/user.selectors';
39
@Component({
410
selector: 'app-users-list',
511
templateUrl: './users-list.component.html',
6-
styleUrls: ['./users-list.component.css']
12+
styleUrls: ['./users-list.component.css'],
713
})
8-
export class UsersListComponent implements OnInit {
14+
export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
15+
users: User[] = [];
16+
isLoading$: Observable<boolean>;
17+
loadUsersSubscription: Subscription;
18+
dtTrigger: Subject<any> = new Subject();
19+
@ViewChild(DataTableDirective, { static: false })
20+
dtElement: DataTableDirective;
921

10-
constructor() { }
22+
constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
23+
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
24+
}
1125

1226
ngOnInit(): void {
27+
this.loadUsersSubscription = this.actionsSubject$
28+
.pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS))
29+
.subscribe((action) => {
30+
this.users = action.payload;
31+
this.rerenderDataTable();
32+
});
33+
this.store.dispatch(new LoadUsers());
1334
}
1435

36+
ngAfterViewInit(): void {
37+
this.rerenderDataTable();
38+
}
39+
40+
ngOnDestroy() {
41+
this.loadUsersSubscription.unsubscribe();
42+
this.dtTrigger.unsubscribe();
43+
}
44+
45+
private rerenderDataTable(): void {
46+
if (this.dtElement && this.dtElement.dtInstance) {
47+
this.dtElement.dtInstance.then((dtInstances: DataTables.Api) => {
48+
dtInstances.destroy();
49+
this.dtTrigger.next();
50+
});
51+
} else {
52+
this.dtTrigger.next();
53+
}
54+
}
55+
56+
changeSwitch() {
57+
console.log('ENTROOO');
58+
}
1559
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export interface User {
2+
name: string;
3+
email: string;
4+
role?: string;
5+
id: string;
6+
tenand_id?: string;
7+
deleted?: string;
8+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { inject, TestBed } from '@angular/core/testing';
2+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
3+
import { UsersService } from './users.service';
4+
5+
describe('UsersService', () => {
6+
let service: UsersService;
7+
let httpMock: HttpTestingController;
8+
9+
beforeEach(() => {
10+
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
11+
service = TestBed.inject(UsersService);
12+
httpMock = TestBed.inject(HttpTestingController);
13+
service.baseUrl = 'users';
14+
});
15+
16+
it('should be created', () => {
17+
expect(service).toBeTruthy();
18+
});
19+
20+
it('services are ready to used', inject(
21+
[HttpClientTestingModule, UsersService],
22+
(httpClient: HttpClientTestingModule, userService: UsersService) => {
23+
expect(httpClient).toBeTruthy();
24+
expect(userService).toBeTruthy();
25+
}
26+
));
27+
28+
it('load all users', () => {
29+
service.loadUsers().subscribe();
30+
31+
const loadUserRequest = httpMock.expectOne(service.baseUrl);
32+
expect(loadUserRequest.request.method).toBe('GET');
33+
});
34+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Injectable } from '@angular/core';
3+
import { Observable } from 'rxjs';
4+
import { environment } from './../../../../environments/environment';
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class UsersService {
9+
constructor(private http: HttpClient) {}
10+
11+
baseUrl = `${environment.timeTrackerApiUrl}/users`;
12+
13+
loadUsers(): Observable<any> {
14+
return this.http.get(this.baseUrl);
15+
}
16+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './user.actions';
2+
export * from './user.reducers';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as actions from './user.actions';
2+
3+
describe('UserActions', () => {
4+
it('LoadUsersSuccess type is UserActionTypes.LOAD_USERS_SUCCESS', () => {
5+
const action = new actions.LoadUsersSuccess([]);
6+
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_SUCCESS);
7+
});
8+
9+
it('LoadUsersFail type is UserActionTypes.LOAD_USERS_FAIL', () => {
10+
const action = new actions.LoadUsersFail('error');
11+
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL);
12+
});
13+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Action } from '@ngrx/store';
2+
import { User } from '../models/users';
3+
4+
export enum UserActionTypes {
5+
LOAD_USERS = '[User] LOAD_USERS',
6+
LOAD_USERS_SUCCESS = '[User] LOAD_USERS_SUCCESS',
7+
LOAD_USERS_FAIL = '[User] LOAD_USERS_FAIL',
8+
}
9+
10+
export class LoadUsers implements Action {
11+
public readonly type = UserActionTypes.LOAD_USERS;
12+
}
13+
14+
export class LoadUsersSuccess implements Action {
15+
readonly type = UserActionTypes.LOAD_USERS_SUCCESS;
16+
constructor(readonly payload: User[]) {}
17+
}
18+
19+
export class LoadUsersFail implements Action {
20+
public readonly type = UserActionTypes.LOAD_USERS_FAIL;
21+
constructor(public error: string) {}
22+
}
23+
24+
export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail;

0 commit comments

Comments
 (0)