Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ActivitiesManagementComponent } from './modules/activities-management/p
import { HomeComponent } from './modules/home/home.component';
import { LoginComponent } from './modules/login/login.component';
import { CustomerComponent } from './modules/customer-management/pages/customer.component';
import { UsersComponent } from './modules/users/pages/users.component';

const routes: Routes = [
{
Expand All @@ -22,6 +23,7 @@ const routes: Routes = [
{ path: 'time-entries', component: TimeEntriesComponent },
{ path: 'activities-management', component: ActivitiesManagementComponent },
{ path: 'customers-management', canActivate: [AdminGuard], component: CustomerComponent },
{ path: 'users', canActivate: [AdminGuard], component: UsersComponent },
{ path: '', pathMatch: 'full', redirectTo: 'time-clock' },
],
},
Expand Down
8 changes: 7 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { ProjectTypeListComponent } from './modules/customer-management/componen
// tslint:disable-next-line: max-line-length
import { CreateProjectTypeComponent } from './modules/customer-management/components/projects-type/components/create-project-type/create-project-type.component';
import { CustomerEffects } from './modules/customer-management/store/customer-management.effects';
import { UserEffects } from './modules/users/store/user.effects';
import { EntryEffects } from './modules/time-clock/store/entry.effects';
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
Expand All @@ -68,6 +69,8 @@ import { TimeRangeFormComponent } from './modules/reports/components/time-range-
import { TimeEntriesTableComponent } from './modules/reports/components/time-entries-table/time-entries-table.component';
import { DialogComponent } from './modules/shared/components/dialog/dialog.component';
import { LoadingBarComponent } from './modules/shared/components/loading-bar/loading-bar.component';
import { UsersComponent } from './modules/users/pages/users.component';
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';

const maskConfig: Partial<IConfig> = {
validation: false,
Expand Down Expand Up @@ -115,7 +118,9 @@ const maskConfig: Partial<IConfig> = {
TimeRangeFormComponent,
TimeEntriesTableComponent,
DialogComponent,
LoadingBarComponent
LoadingBarComponent,
UsersComponent,
UsersListComponent
],
imports: [
NgxMaskModule.forRoot(maskConfig),
Expand Down Expand Up @@ -144,6 +149,7 @@ const maskConfig: Partial<IConfig> = {
TechnologyEffects,
ProjectTypeEffects,
EntryEffects,
UserEffects,
]),
ToastrModule.forRoot()
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('SidebarComponent', () => {
component.getSidebarItems();
const menuItems = component.itemsSidebar;

expect(menuItems.length).toBe(5);
expect(menuItems.length).toBe(6);
});

it('non admin users have two menu items', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class SidebarComponent implements OnInit {
{route: '/reports', icon: 'fas fa-chart-pie', text: 'Reports', active: false},
{route: '/activities-management', icon: 'fas fa-file-alt', text: 'Activities', active: false},
{route: '/customers-management', icon: 'fas fa-user', text: 'Customers', active: false},
{route: '/users', icon: 'fas fa-user', text: 'Users', active: false},
];
} else {
this.itemsSidebar = [
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<table *ngIf="users" class="table table-sm table-bordered table-striped mb-0" datatable [dtTrigger]="dtTrigger">
<thead class="thead-blue">
<tr class="d-flex">
<th class="col-6">User Email</th>
<th class="col-6">Names</th>
</tr>
</thead>
<app-loading-bar *ngIf="isLoading$ | async"></app-loading-bar>
<tbody *ngIf="(isLoading$ | async) === false">
<tr class="d-flex" *ngFor="let user of users">
<td class="col-sm-6">{{ user.email }}</td>
<td class="col-sm-6">{{ user.name }}</td>
</tr>
</tbody>
</table>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';

import { NgxPaginationModule } from 'ngx-pagination';
import { UsersListComponent } from './users-list.component';
import { UserActionTypes, UserState, LoadUsers } from '../../store';
import { ActionsSubject } from '@ngrx/store';
import { DataTablesModule } from 'angular-datatables';

describe('UsersListComponent', () => {
let component: UsersListComponent;
let fixture: ComponentFixture<UsersListComponent>;
let store: MockStore<UserState>;
const actionSub: ActionsSubject = new ActionsSubject();

const state: UserState = {
data: [{ name: 'name', email: 'email', role: 'role', id: 'id', tenant_id: 'tenant id', deleted: 'delete' }],
isLoading: false,
message: '',
};

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NgxPaginationModule, DataTablesModule],
declarations: [UsersListComponent],
providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(UsersListComponent);
component = fixture.componentInstance;
store = TestBed.inject(MockStore);
store.setState(state);
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('when the component is initialized the load User action is triggered', () => {
spyOn(store, 'dispatch');

component.ngOnInit();

expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers());
});

it('on success load users, the user list should be populated', () => {
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
const action = {
type: UserActionTypes.LOAD_USERS_SUCCESS,
payload: state.data,
};

actionSubject.next(action);

expect(component.users).toEqual(state.data);
});

it('on success load users, the datatable should be reloaded', async () => {
const actionSubject = TestBed.inject(ActionsSubject);
const action = {
type: UserActionTypes.LOAD_USERS_SUCCESS,
payload: state.data,
};
spyOn(component.dtElement.dtInstance, 'then');

actionSubject.next(action);

expect(component.dtElement.dtInstance.then).toHaveBeenCalled();
});

afterEach(() => {
component.dtTrigger.unsubscribe();
component.loadUsersSubscription.unsubscribe();
fixture.destroy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActionsSubject, select, Store } from '@ngrx/store';
import { DataTableDirective } from 'angular-datatables';
import { Observable, Subject, Subscription } from 'rxjs';
import { delay, filter } from 'rxjs/operators';
import { User } from '../../models/users';
import { LoadUsers, UserActionTypes } from '../../store/user.actions';
import { getIsLoading } from '../../store/user.selectors';
@Component({
selector: 'app-users-list',
templateUrl: './users-list.component.html',
styleUrls: ['./users-list.component.css'],
})
export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit {
users: User[] = [];
isLoading$: Observable<boolean>;
loadUsersSubscription: Subscription;
dtTrigger: Subject<any> = new Subject();
@ViewChild(DataTableDirective, { static: false })
dtElement: DataTableDirective;

constructor(private store: Store<User>, private actionsSubject$: ActionsSubject) {
this.isLoading$ = store.pipe(delay(0), select(getIsLoading));
}

ngOnInit(): void {
this.loadUsersSubscription = this.actionsSubject$
.pipe(filter((action: any) => action.type === UserActionTypes.LOAD_USERS_SUCCESS))
.subscribe((action) => {
this.users = action.payload;
this.rerenderDataTable();
});
this.store.dispatch(new LoadUsers());
}

ngAfterViewInit(): void {
this.rerenderDataTable();
}

ngOnDestroy() {
this.loadUsersSubscription.unsubscribe();
this.dtTrigger.unsubscribe();
}

private rerenderDataTable(): void {
if (this.dtElement && this.dtElement.dtInstance) {
this.dtElement.dtInstance.then((dtInstances: DataTables.Api) => {
dtInstances.destroy();
this.dtTrigger.next();
});
} else {
this.dtTrigger.next();
}
}
}
8 changes: 8 additions & 0 deletions src/app/modules/users/models/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface User {
name: string;
email: string;
role?: string;
id: string;
tenant_id?: string;
deleted?: string;
}
Empty file.
1 change: 1 addition & 0 deletions src/app/modules/users/pages/users.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<app-users-list></app-users-list>
25 changes: 25 additions & 0 deletions src/app/modules/users/pages/users.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { UsersComponent } from './users.component';

describe('UsersComponent', () => {
let component: UsersComponent;
let fixture: ComponentFixture<UsersComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UsersComponent ]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(UsersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
10 changes: 10 additions & 0 deletions src/app/modules/users/pages/users.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';

@Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css'],
})
export class UsersComponent {
constructor() {}
}
34 changes: 34 additions & 0 deletions src/app/modules/users/services/users.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { inject, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UsersService } from './users.service';

describe('UsersService', () => {
let service: UsersService;
let httpMock: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
service = TestBed.inject(UsersService);
httpMock = TestBed.inject(HttpTestingController);
service.baseUrl = 'users';
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('services are ready to used', inject(
[HttpClientTestingModule, UsersService],
(httpClient: HttpClientTestingModule, userService: UsersService) => {
expect(httpClient).toBeTruthy();
expect(userService).toBeTruthy();
}
));

it('load all users', () => {
service.loadUsers().subscribe();

const loadUserRequest = httpMock.expectOne(service.baseUrl);
expect(loadUserRequest.request.method).toBe('GET');
});
});
16 changes: 16 additions & 0 deletions src/app/modules/users/services/users.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from './../../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class UsersService {
constructor(private http: HttpClient) {}

baseUrl = `${environment.timeTrackerApiUrl}/users`;

loadUsers(): Observable<any> {
return this.http.get(this.baseUrl);
}
}
2 changes: 2 additions & 0 deletions src/app/modules/users/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './user.actions';
export * from './user.reducers';
18 changes: 18 additions & 0 deletions src/app/modules/users/store/user.actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as actions from './user.actions';

describe('UserActions', () => {
it('LoadUsers type is UserActionTypes.LOAD_USERS', () => {
const action = new actions.LoadUsers();
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS);
});

it('LoadUsersSuccess type is UserActionTypes.LOAD_USERS_SUCCESS', () => {
const action = new actions.LoadUsersSuccess([]);
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_SUCCESS);
});

it('LoadUsersFail type is UserActionTypes.LOAD_USERS_FAIL', () => {
const action = new actions.LoadUsersFail('error');
expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL);
});
});
24 changes: 24 additions & 0 deletions src/app/modules/users/store/user.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Action } from '@ngrx/store';
import { User } from '../models/users';

export enum UserActionTypes {
LOAD_USERS = '[User] LOAD_USERS',
LOAD_USERS_SUCCESS = '[User] LOAD_USERS_SUCCESS',
LOAD_USERS_FAIL = '[User] LOAD_USERS_FAIL',
}

export class LoadUsers implements Action {
public readonly type = UserActionTypes.LOAD_USERS;
}

export class LoadUsersSuccess implements Action {
readonly type = UserActionTypes.LOAD_USERS_SUCCESS;
constructor(readonly payload: User[]) {}
}

export class LoadUsersFail implements Action {
public readonly type = UserActionTypes.LOAD_USERS_FAIL;
constructor(public error: string) {}
}

export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail;
Loading