From d3f94f125013d11a47b3acd1ef27c7cc23e696ff Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Fri, 15 Jan 2021 17:26:45 -0500 Subject: [PATCH 1/7] fix: TT-104 update format commit --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d7e31bb05..9233a5d8f 100644 --- a/README.md +++ b/README.md @@ -71,11 +71,17 @@ Install the following extensions: - **test**: Adding missing tests or correcting existing tests. ### Example - TT-48 fix: #48 implement semantic versioning. + fix: TT-48 implement semantic versioning Prefix to use in the space fix: `(fix: |feat: |perf: |build: |ci: |docs: |refactor: |style: |test: )` +| Commit message | Release type | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| `fix(pencil): stop graphite breaking when too much pressure applied` | Patch Release | +| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release | +| `perf(pencil): remove graphiteWidth option`

`BREAKING CHANGE: The graphiteWidth option has been removed.`
`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release | + ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. @@ -116,6 +122,4 @@ You can visit the app in the following link: ## Further help -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). - -## Test to run semantic release \ No newline at end of file +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). \ No newline at end of file From a2ba48ec2fc44bfa7b6e1a84a00c1496e6deb8b9 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Fri, 15 Jan 2021 17:28:04 -0500 Subject: [PATCH 2/7] fix: TT-104 update format commit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e236394fb..fd577a34f 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "config": { "commit-message-validator": { "pattern": "^(fix: TT-|feat: TT-|perf: TT-|build: TT-|ci: TT-|docs: TT-|refactor: TT-|style: TT-|test: TT-)[0-9].*", - "errorMessage": "Your commit message needs to start with TT-number fix:, feat:, or perf: followed by any commit message, e.g. TT-43 fix: any commit message" + "errorMessage": "Your commit message needs to start with fix: , feat:, or perf: followed by any commit message, e.g. fix: TT-43 any commit message" } }, "resolutions": { From 106fb9f0e81a6cfd79282ff8f0395e8947576f1a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 15 Jan 2021 22:33:12 +0000 Subject: [PATCH 3/7] chore(release): 1.31.6 [skip ci]nn --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a586e16c..7b895bb5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.31.5", + "version": "1.31.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fd577a34f..27afea211 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.31.5", + "version": "1.31.6", "scripts": { "preinstall": "npx npm-force-resolutions", "ng": "ng", From d198dd0473cdd496d26a77dea8e0f5621de72035 Mon Sep 17 00:00:00 2001 From: PaulRC-ioet <73141380+PaulRC-ioet@users.noreply.github.com> Date: Mon, 18 Jan 2021 13:40:08 -0500 Subject: [PATCH 4/7] Tt 97 grant or revoke roles to users (#626) * TT-97 feat: add grant or revoke role admin to users * TT-97 fix: refactor code * fix: TT-97 add some test to feature * fix: TT-97 Add new tests and fix a typo errors * fix: TT-97 fix typo errors * fix: TT-97 Remove a function failed --- angular.json | 1 + package-lock.json | 35 +++--- package.json | 5 +- src/app/app.module.ts | 2 + .../users-list/users-list.component.html | 32 ++++- .../users-list/users-list.component.spec.ts | 112 ++++++++++++++++-- .../users-list/users-list.component.ts | 47 +++++++- .../users/services/users.service.spec.ts | 20 ++++ .../modules/users/services/users.service.ts | 10 ++ .../modules/users/store/user.actions.spec.ts | 37 ++++++ src/app/modules/users/store/user.actions.ts | 47 +++++++- .../modules/users/store/user.effects.spec.ts | 54 +++++++++ src/app/modules/users/store/user.effects.ts | 36 ++++++ .../modules/users/store/user.reducer.spec.ts | 65 ++++++++++ src/app/modules/users/store/user.reducers.ts | 53 +++++++++ 15 files changed, 519 insertions(+), 37 deletions(-) diff --git a/angular.json b/angular.json index 5add0a5ba..4988f5368 100644 --- a/angular.json +++ b/angular.json @@ -29,6 +29,7 @@ "./node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.scss", "node_modules/ngx-toastr/toastr.css", + "./node_modules/ngx-ui-switch/ui-switch.component.css", "node_modules/datatables.net-buttons-dt/css/buttons.dataTables.css" ], "scripts": [ diff --git a/package-lock.json b/package-lock.json index 7b895bb5c..5abb2b1c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6077,15 +6077,6 @@ "p-try": "^2.0.0" } }, - "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, "ssri": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.0.tgz", @@ -11068,6 +11059,11 @@ "tslib": "^1.10.0" } }, + "ngx-ui-switch": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-10.0.2.tgz", + "integrity": "sha512-T8LhlRnjm36kElVNxkzEmpiXA82FO74FGJHpsW60U6CFXjWqlEiQAXuCA8c/qiWDxHqy36KkaxY8/cutv487RA==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18536,6 +18532,15 @@ } } }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20010,9 +20015,9 @@ } }, "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" @@ -21245,9 +21250,9 @@ }, "dependencies": { "serialize-javascript": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", - "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", "dev": true, "requires": { "randombytes": "^2.1.0" diff --git a/package.json b/package.json index 27afea211..f5411847c 100644 --- a/package.json +++ b/package.json @@ -22,13 +22,13 @@ "@angular/platform-browser": "~10.2.2", "@angular/platform-browser-dynamic": "~10.2.2", "@angular/router": "~10.2.2", + "@azure/app-configuration": "^1.1.0", + "@azure/identity": "^1.1.0", "@ngrx/effects": "^10.0.1", "@ngrx/store": "^10.0.1", "@ngrx/store-devtools": "^10.0.1", "@types/datatables.net-buttons": "^1.4.3", "angular-datatables": "^9.0.2", - "@azure/app-configuration": "^1.1.0", - "@azure/identity": "^1.1.0", "bootstrap": "^4.4.1", "datatables.net": "^1.10.21", "datatables.net-buttons": "^1.6.2", @@ -46,6 +46,7 @@ "ngx-material-timepicker": "^5.5.3", "ngx-pagination": "^5.0.0", "ngx-toastr": "^12.0.1", + "ngx-ui-switch": "^10.0.2", "rxjs": "~6.6.3", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1da328b83..505a1d050 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -72,6 +72,7 @@ import { DialogComponent } from './modules/shared/components/dialog/dialog.compo 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'; +import { UiSwitchModule } from 'ngx-ui-switch'; import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker'; // tslint:disable-next-line: max-line-length import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component'; @@ -142,6 +143,7 @@ const maskConfig: Partial = { DataTablesModule, AutocompleteLibModule, NgxMaterialTimepickerModule, + UiSwitchModule, StoreModule.forRoot(reducers, { metaReducers, }), diff --git a/src/app/modules/users/components/users-list/users-list.component.html b/src/app/modules/users/components/users-list/users-list.component.html index 3b4490c97..71aa110da 100644 --- a/src/app/modules/users/components/users-list/users-list.component.html +++ b/src/app/modules/users/components/users-list/users-list.component.html @@ -1,15 +1,37 @@ - +
- - + + + - - + + +
User EmailNamesUser EmailNamesRoles
{{ user.email }}{{ user.name }}{{ user.email }}{{ user.name }} +
+ + admin + + test +
+
diff --git a/src/app/modules/users/components/users-list/users-list.component.spec.ts b/src/app/modules/users/components/users-list/users-list.component.spec.ts index 8cd39d44a..a8e7ead89 100644 --- a/src/app/modules/users/components/users-list/users-list.component.spec.ts +++ b/src/app/modules/users/components/users-list/users-list.component.spec.ts @@ -3,14 +3,17 @@ 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 { UserActionTypes, UserState, LoadUsers, GrantRoleUser, RevokeRoleUser } from '../../store'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; import { ActionsSubject } from '@ngrx/store'; import { DataTablesModule } from 'angular-datatables'; +import { Observable, of } from 'rxjs'; describe('UsersListComponent', () => { let component: UsersListComponent; let fixture: ComponentFixture; let store: MockStore; + let featureManagerService: FeatureManagerService; const actionSub: ActionsSubject = new ActionsSubject(); const state: UserState = { @@ -29,13 +32,16 @@ describe('UsersListComponent', () => { message: '', }; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [NgxPaginationModule, DataTablesModule], - declarations: [UsersListComponent], - providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }], - }).compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NgxPaginationModule, DataTablesModule], + declarations: [UsersListComponent], + providers: [provideMockStore({ initialState: state }), { provide: ActionsSubject, useValue: actionSub }], + }).compileComponents(); + featureManagerService = TestBed.inject(FeatureManagerService); + }) + ); beforeEach(() => { fixture = TestBed.createComponent(UsersListComponent); @@ -69,6 +75,76 @@ describe('UsersListComponent', () => { expect(component.users).toEqual(state.data); }); + it('When Component is created, should call the feature toggle method', () => { + spyOn(component, 'isFeatureToggleActivated').and.returnValue(of(true)); + + component.ngOnInit(); + + expect(component.isFeatureToggleActivated).toHaveBeenCalled(); + expect(component.isUserRoleToggleOn).toBe(true); + }); + + const actionsParams = [ + { actionType: UserActionTypes.GRANT_USER_ROLE_SUCCESS }, + { actionType: UserActionTypes.REVOKE_USER_ROLE_SUCCESS }, + ]; + + actionsParams.map((param) => { + it(`When action ${param.actionType} is dispatched should triggered load Users action`, () => { + spyOn(store, 'dispatch'); + + const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; + const action = { + type: param.actionType, + payload: state.data, + }; + + actionSubject.next(action); + + expect(store.dispatch).toHaveBeenCalledWith(new LoadUsers()); + }); + }); + + const grantRoleTypes = [ + { roleId: 'admin', roleValue: 'time-tracker-admin' }, + { roleId: 'test', roleValue: 'time-tracker-tester' }, + ]; + + grantRoleTypes.map((param) => { + it(`When user switchRole to ${param.roleId} and don't have any role, should grant ${param.roleValue} Role`, () => { + const roleId = param.roleId; + const roleValue = param.roleValue; + const userRoles = []; + const userId = 'userId'; + + spyOn(store, 'dispatch'); + + component.switchRole(userId, userRoles, roleId, roleValue); + + expect(store.dispatch).toHaveBeenCalledWith(new GrantRoleUser(userId, roleId)); + }); + }); + + const revokeRoleTypes = [ + { roleId: 'admin', roleValue: 'time-tracker-admin', userRoles: ['time-tracker-admin'] }, + { roleId: 'test', roleValue: 'time-tracker-tester', userRoles: ['time-tracker-tester'] }, + ]; + + revokeRoleTypes.map((param) => { + it(`When user switchRole to ${param.roleId} and have that rol asigned, should revoke ${param.roleValue} Role`, () => { + const roleId = param.roleId; + const roleValue = param.roleValue; + const userRoles = param.userRoles; + const userId = 'userId'; + + spyOn(store, 'dispatch'); + + component.switchRole(userId, userRoles, roleId, roleValue); + + expect(store.dispatch).toHaveBeenCalledWith(new RevokeRoleUser(userId, roleId)); + }); + }); + it('on success load users, the data of roles should be an array and role null', () => { const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject; const action = { @@ -114,7 +190,23 @@ describe('UsersListComponent', () => { }); }); - it('on success load users, the datatable should be reloaded', async () => { + const toggleValues = [true, false]; + toggleValues.map((toggleValue) => { + it(`when FeatureToggle is ${toggleValue} should return ${toggleValue}`, () => { + spyOn(featureManagerService, 'isToggleEnabledForUser').and.returnValue(of(toggleValue)); + + const isFeatureToggleActivated: Observable = component.isFeatureToggleActivated(); + + expect(featureManagerService.isToggleEnabledForUser).toHaveBeenCalled(); + isFeatureToggleActivated.subscribe((value) => expect(value).toEqual(toggleValue)); + }); + }); + + /* + TODO: block commented on purpose so that when the tests pass and the Feature toggle is removed, + the table will be rendered again with dtInstance and not with dtOptions + + it('on success load users, the datatable should be reloaded', async () => { const actionSubject = TestBed.inject(ActionsSubject); const action = { type: UserActionTypes.LOAD_USERS_SUCCESS, @@ -125,7 +217,7 @@ describe('UsersListComponent', () => { actionSubject.next(action); expect(component.dtElement.dtInstance.then).toHaveBeenCalled(); - }); + });*/ afterEach(() => { component.dtTrigger.unsubscribe(); diff --git a/src/app/modules/users/components/users-list/users-list.component.ts b/src/app/modules/users/components/users-list/users-list.component.ts index 5d119fd47..91cfc2e23 100644 --- a/src/app/modules/users/components/users-list/users-list.component.ts +++ b/src/app/modules/users/components/users-list/users-list.component.ts @@ -2,10 +2,12 @@ import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular 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 { delay, filter, map } from 'rxjs/operators'; import { User } from '../../models/users'; -import { LoadUsers, UserActionTypes } from '../../store/user.actions'; +import { GrantRoleUser, LoadUsers, RevokeRoleUser, UserActionTypes } from '../../store/user.actions'; import { getIsLoading } from '../../store/user.selectors'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; + @Component({ selector: 'app-users-list', templateUrl: './users-list.component.html', @@ -15,22 +17,45 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit { users: User[] = []; isLoading$: Observable; loadUsersSubscription: Subscription; + switchRoleSubscription: Subscription; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) dtElement: DataTableDirective; + dtOptions: any = {}; + isUserRoleToggleOn; - constructor(private store: Store, private actionsSubject$: ActionsSubject) { + constructor( + private store: Store, + private actionsSubject$: ActionsSubject, + private featureManagerService: FeatureManagerService + ) { this.isLoading$ = store.pipe(delay(0), select(getIsLoading)); } ngOnInit(): void { + this.isFeatureToggleActivated().subscribe((flag) => { + this.isUserRoleToggleOn = flag; + }); + this.store.dispatch(new LoadUsers()); 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()); + + this.switchRoleSubscription = this.actionsSubject$ + .pipe( + filter( + (action: any) => + action.type === UserActionTypes.GRANT_USER_ROLE_SUCCESS || + action.type === UserActionTypes.REVOKE_USER_ROLE_SUCCESS + ) + ) + .subscribe((action) => { + this.store.dispatch(new LoadUsers()); + this.rerenderDataTable(); + }); } ngAfterViewInit(): void { @@ -52,4 +77,18 @@ export class UsersListComponent implements OnInit, OnDestroy, AfterViewInit { this.dtTrigger.next(); } } + + switchRole(userId: string, userRoles: string[], roleId: string, roleValue: string) { + userRoles.includes(roleValue) + ? this.store.dispatch(new RevokeRoleUser(userId, roleId)) + : this.store.dispatch(new GrantRoleUser(userId, roleId)); + } + + isFeatureToggleActivated() { + return this.featureManagerService.isToggleEnabledForUser('ui-list-test-users').pipe( + map((enabled) => { + return enabled === true ? true : false; + }) + ); + } } diff --git a/src/app/modules/users/services/users.service.spec.ts b/src/app/modules/users/services/users.service.spec.ts index 6489f34b9..cba99ef44 100644 --- a/src/app/modules/users/services/users.service.spec.ts +++ b/src/app/modules/users/services/users.service.spec.ts @@ -31,4 +31,24 @@ describe('UsersService', () => { const loadUserRequest = httpMock.expectOne(service.baseUrl); expect(loadUserRequest.request.method).toBe('GET'); }); + + it('grant role to a User', () => { + const userId = 'userId'; + const roleId = 'admin'; + + service.grantRole(userId, roleId).subscribe(); + + const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/grant`); + expect(grantRoleRequest.request.method).toBe('POST'); + }); + + it('revoke role to a User', () => { + const userId = 'userId'; + const roleId = 'admin'; + + service.revokeRole(userId, roleId).subscribe(); + + const grantRoleRequest = httpMock.expectOne(`${service.baseUrl}/${userId}/roles/${roleId}/revoke`); + expect(grantRoleRequest.request.method).toBe('POST'); + }); }); diff --git a/src/app/modules/users/services/users.service.ts b/src/app/modules/users/services/users.service.ts index e41b0401a..80e181f9e 100644 --- a/src/app/modules/users/services/users.service.ts +++ b/src/app/modules/users/services/users.service.ts @@ -13,4 +13,14 @@ export class UsersService { loadUsers(): Observable { return this.http.get(this.baseUrl); } + + grantRole(userId: string, roleId: string): Observable { + const url = `${this.baseUrl}/${userId}/roles/${roleId}/grant`; + return this.http.post(url, null); + } + + revokeRole(userId: string, roleId: string): Observable { + const url = `${this.baseUrl}/${userId}/roles/${roleId}/revoke`; + return this.http.post(url, null); + } } diff --git a/src/app/modules/users/store/user.actions.spec.ts b/src/app/modules/users/store/user.actions.spec.ts index 6edb98176..aa66253be 100644 --- a/src/app/modules/users/store/user.actions.spec.ts +++ b/src/app/modules/users/store/user.actions.spec.ts @@ -1,4 +1,5 @@ import * as actions from './user.actions'; +import { User } from '../models/users'; describe('UserActions', () => { it('LoadUsers type is UserActionTypes.LOAD_USERS', () => { @@ -15,4 +16,40 @@ describe('UserActions', () => { const action = new actions.LoadUsersFail('error'); expect(action.type).toEqual(actions.UserActionTypes.LOAD_USERS_FAIL); }); + + it('GrantRoleUser type is UserActionTypes.GRANT_USER_ROLE', () => { + const UserId = 'UserId'; + const RoleId = 'RoleId'; + const action = new actions.GrantRoleUser(UserId, RoleId); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE); + }); + + it('GrantRoleUserSuccess type is UserActionTypes.GRANT_USER_ROLE_SUCCESS', () => { + const payload: User = { id: 'id', email: 'email', name: 'name' }; + const action = new actions.GrantRoleUserSuccess(payload); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_SUCCESS); + }); + + it('GrantRoleUserFail type is UserActionTypes.GRANT_USER_ROLE_FAIL', () => { + const action = new actions.GrantRoleUserFail('error'); + expect(action.type).toEqual(actions.UserActionTypes.GRANT_USER_ROLE_FAIL); + }); + + it('RevokeRoleUser type is UserActionTypes.REVOKE_USER_ROLE', () => { + const UserId = 'UserId'; + const RoleId = 'RoleId'; + const action = new actions.RevokeRoleUser(UserId, RoleId); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE); + }); + + it('RevokeRoleUserSuccess type is UserActionTypes.REVOKE_USER_ROLE_SUCCESS', () => { + const payload: User = { id: 'id', email: 'email', name: 'name' }; + const action = new actions.RevokeRoleUserSuccess(payload); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_SUCCESS); + }); + + it('RevokeRoleUserFail type is UserActionTypes.REVOKE_USER_ROLE_FAIL', () => { + const action = new actions.RevokeRoleUserFail('error'); + expect(action.type).toEqual(actions.UserActionTypes.REVOKE_USER_ROLE_FAIL); + }); }); diff --git a/src/app/modules/users/store/user.actions.ts b/src/app/modules/users/store/user.actions.ts index e6419423f..02ee3fe94 100644 --- a/src/app/modules/users/store/user.actions.ts +++ b/src/app/modules/users/store/user.actions.ts @@ -5,6 +5,12 @@ export enum UserActionTypes { LOAD_USERS = '[User] LOAD_USERS', LOAD_USERS_SUCCESS = '[User] LOAD_USERS_SUCCESS', LOAD_USERS_FAIL = '[User] LOAD_USERS_FAIL', + GRANT_USER_ROLE = '[User] GRANT_USER_ROLE', + GRANT_USER_ROLE_SUCCESS = '[User] GRANT_USER_ROLE_SUCCESS', + GRANT_USER_ROLE_FAIL = '[User] GRANT_USER_ROLE_FAIL', + REVOKE_USER_ROLE = '[User] REVOKE_USER_ROLE', + REVOKE_USER_ROLE_SUCCESS = '[User] REVOKE_USER_ROLE_SUCCESS', + REVOKE_USER_ROLE_FAIL = '[User] REVOKE_USER_ROLE_FAIL', DEFAULT_USER = '[USER] DEFAULT_USER', } @@ -22,8 +28,47 @@ export class LoadUsersFail implements Action { constructor(public error: string) {} } +export class GrantRoleUser implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE; + constructor(public userId: string, public roleId: string) {} +} + +export class GrantRoleUserSuccess implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE_SUCCESS; + constructor(public payload: User) {} +} + +export class GrantRoleUserFail implements Action { + public readonly type = UserActionTypes.GRANT_USER_ROLE_FAIL; + constructor(public error: string) {} +} + +export class RevokeRoleUser implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE; + constructor(public userId: string, public roleId: string) {} +} + +export class RevokeRoleUserSuccess implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE_SUCCESS; + constructor(public payload: User) {} +} + +export class RevokeRoleUserFail implements Action { + public readonly type = UserActionTypes.REVOKE_USER_ROLE_FAIL; + constructor(public error: string) {} +} export class DefaultUser implements Action { public readonly type = UserActionTypes.DEFAULT_USER; } -export type UserActions = LoadUsers | LoadUsersSuccess | LoadUsersFail | DefaultUser; +export type UserActions = + | LoadUsers + | LoadUsersSuccess + | LoadUsersFail + | DefaultUser + | GrantRoleUser + | GrantRoleUserSuccess + | GrantRoleUserFail + | RevokeRoleUser + | RevokeRoleUserSuccess + | RevokeRoleUserFail; diff --git a/src/app/modules/users/store/user.effects.spec.ts b/src/app/modules/users/store/user.effects.spec.ts index 2ac02426a..4438f64ea 100644 --- a/src/app/modules/users/store/user.effects.spec.ts +++ b/src/app/modules/users/store/user.effects.spec.ts @@ -51,4 +51,58 @@ describe('UserEffects', () => { expect(action.type).toEqual(UserActionTypes.LOAD_USERS_FAIL); }); }); + + it('action type is GRANT_USER_ROLE_SUCCESS when service is executed sucessfully', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.GRANT_USER_ROLE, userId, roleId }); + const serviceSpy = spyOn(service, 'grantRole'); + spyOn(toastrService, 'success'); + serviceSpy.and.returnValue(of(user)); + + effects.grantUserRole$.subscribe((action) => { + expect(toastrService.success).toHaveBeenCalledWith('Grant User Role Success'); + expect(action.type).toEqual(UserActionTypes.GRANT_USER_ROLE_SUCCESS); + }); + }); + + it('action type is GRANT_USER_ROLE_FAIL when service is executed and fail', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.GRANT_USER_ROLE, userId, roleId }); + spyOn(service, 'grantRole').and.returnValue(throwError({ error: { message: 'error' } })); + spyOn(toastrService, 'error'); + + effects.grantUserRole$.subscribe((action) => { + expect(toastrService.error).toHaveBeenCalled(); + expect(action.type).toEqual(UserActionTypes.GRANT_USER_ROLE_FAIL); + }); + }); + + it('action type is REVOKE_USER_ROLE_SUCCESS when service is executed sucessfully', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.REVOKE_USER_ROLE, userId, roleId }); + const serviceSpy = spyOn(service, 'revokeRole'); + spyOn(toastrService, 'success'); + serviceSpy.and.returnValue(of(user)); + + effects.revokeUserRole$.subscribe((action) => { + expect(toastrService.success).toHaveBeenCalledWith('Revoke User Role Success'); + expect(action.type).toEqual(UserActionTypes.REVOKE_USER_ROLE_SUCCESS); + }); + }); + + it('action type is REVOKE_USER_ROLE_FAIL when service is executed and fail', async () => { + const userId = 'userId'; + const roleId = 'roleId'; + actions$ = of({ type: UserActionTypes.REVOKE_USER_ROLE, userId, roleId }); + spyOn(service, 'revokeRole').and.returnValue(throwError({ error: { message: 'error' } })); + spyOn(toastrService, 'error'); + + effects.revokeUserRole$.subscribe((action) => { + expect(toastrService.error).toHaveBeenCalled(); + expect(action.type).toEqual(UserActionTypes.REVOKE_USER_ROLE_FAIL); + }); + }); }); diff --git a/src/app/modules/users/store/user.effects.ts b/src/app/modules/users/store/user.effects.ts index 8ed2e1be5..fe715282c 100644 --- a/src/app/modules/users/store/user.effects.ts +++ b/src/app/modules/users/store/user.effects.ts @@ -27,4 +27,40 @@ export class UserEffects { ) ) ); + + @Effect() + grantUserRole$: Observable = this.actions$.pipe( + ofType(actions.UserActionTypes.GRANT_USER_ROLE), + map((action: actions.GrantRoleUser) => action), + mergeMap((action) => + this.userService.grantRole(action.userId, action.roleId).pipe( + map((response) => { + this.toastrService.success('Grant User Role Success'); + return new actions.GrantRoleUserSuccess(response); + }), + catchError((error) => { + this.toastrService.error(error.error.message); + return of(new actions.GrantRoleUserFail(error)); + }) + ) + ) + ); + + @Effect() + revokeUserRole$: Observable = this.actions$.pipe( + ofType(actions.UserActionTypes.REVOKE_USER_ROLE), + map((action: actions.RevokeRoleUser) => action), + mergeMap((action) => + this.userService.revokeRole(action.userId, action.roleId).pipe( + map((response) => { + this.toastrService.success('Revoke User Role Success'); + return new actions.RevokeRoleUserSuccess(response); + }), + catchError((error) => { + this.toastrService.error(error.error.message); + return of(new actions.RevokeRoleUserFail(error)); + }) + ) + ) + ); } diff --git a/src/app/modules/users/store/user.reducer.spec.ts b/src/app/modules/users/store/user.reducer.spec.ts index ccb8b69f8..4f89e5d4e 100644 --- a/src/app/modules/users/store/user.reducer.spec.ts +++ b/src/app/modules/users/store/user.reducer.spec.ts @@ -1,4 +1,5 @@ import { UserState, userReducer } from './user.reducers'; +import { User } from '../models/users'; import * as actions from './user.actions'; describe('userReducer', () => { @@ -28,6 +29,70 @@ describe('userReducer', () => { expect(state.data.length).toBe(0); }); + it('on GrantUserRole, isLoading is true', () => { + const userId = 'userId'; + const roleId = 'roleId'; + const action = new actions.GrantRoleUser(userId, roleId); + const state = userReducer(initialState, action); + + expect(state.isLoading).toEqual(true); + }); + + it('on GrantRoleUserSuccess, user role should change', () => { + const currentState: UserState = { + data: [{ id: 'id', name: 'name', email: 'email', role: null }], + isLoading: false, + message: '', + }; + const userGranted: User = { id: 'id', name: 'name', email: 'email', role: 'admin' }; + const action = new actions.GrantRoleUserSuccess(userGranted); + const state = userReducer(currentState, action); + + expect(state.data).toEqual([userGranted]); + expect(state.isLoading).toEqual(false); + expect(state.message).toEqual('Grant User Role Success'); + }); + + it('on GrantRoleUserFail, should show a message with an error message', () => { + const action = new actions.GrantRoleUserFail('error'); + const state = userReducer(initialState, action); + + expect(state.message).toEqual('Something went wrong granting user role'); + expect(state.isLoading).toEqual(false); + }); + + it('on RevokeUserRole, isLoading is true', () => { + const userId = 'userId'; + const roleId = 'roleId'; + const action = new actions.RevokeRoleUser(userId, roleId); + const state = userReducer(initialState, action); + + expect(state.isLoading).toEqual(true); + }); + + it('on RevokeRoleUserSuccess, user role should change', () => { + const currentState: UserState = { + data: [{ id: 'id', name: 'name', email: 'email', role: 'admin' }], + isLoading: false, + message: '', + }; + const userRevoked: User = { id: 'id', name: 'name', email: 'email', role: null }; + const action = new actions.RevokeRoleUserSuccess(userRevoked); + const state = userReducer(currentState, action); + + expect(state.data).toEqual([userRevoked]); + expect(state.isLoading).toEqual(false); + expect(state.message).toEqual('Revoke User Role Success'); + }); + + it('on RevokeRoleUserFail, should show a message with an error message', () => { + const action = new actions.RevokeRoleUserFail('error'); + const state = userReducer(initialState, action); + + expect(state.message).toEqual('Something went wrong revoking user role'); + expect(state.isLoading).toEqual(false); + }); + it('on Default, ', () => { const action = new actions.DefaultUser(); const state = userReducer(initialState, action); diff --git a/src/app/modules/users/store/user.reducers.ts b/src/app/modules/users/store/user.reducers.ts index 8bd0d10e7..40bd5ca87 100644 --- a/src/app/modules/users/store/user.reducers.ts +++ b/src/app/modules/users/store/user.reducers.ts @@ -14,6 +14,7 @@ export const initialState: UserState = { }; export const userReducer = (state: UserState = initialState, action: UserActions) => { + const userData = [...state.data]; switch (action.type) { case UserActionTypes.LOAD_USERS: { return { @@ -33,6 +34,58 @@ export const userReducer = (state: UserState = initialState, action: UserActions ...state, data: [], isLoading: false, + message: action.error, + }; + } + case UserActionTypes.GRANT_USER_ROLE: { + return { + ...state, + isLoading: true, + }; + } + case UserActionTypes.GRANT_USER_ROLE_SUCCESS: { + const index = userData.findIndex((user) => user.id === action.payload.id); + userData[index] = action.payload; + return { + ...state, + data: userData, + isLoading: false, + message: 'Grant User Role Success', + }; + } + + case UserActionTypes.GRANT_USER_ROLE_FAIL: { + return { + ...state, + data: state.data, + isLoading: false, + message: 'Something went wrong granting user role', + }; + } + + case UserActionTypes.REVOKE_USER_ROLE: { + return { + ...state, + isLoading: true, + }; + } + case UserActionTypes.REVOKE_USER_ROLE_SUCCESS: { + const index = userData.findIndex((user) => user.id === action.payload.id); + userData[index] = action.payload; + return { + ...state, + data: userData, + isLoading: false, + message: 'Revoke User Role Success', + }; + } + + case UserActionTypes.REVOKE_USER_ROLE_FAIL: { + return { + ...state, + data: state.data, + isLoading: false, + message: 'Something went wrong revoking user role', }; } default: From 8b5a239130736aef29b42b8f96b9559d5a5fe5e3 Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Fri, 15 Jan 2021 21:33:42 -0500 Subject: [PATCH 5/7] fix: TT-117 two-entries-in-progress-bug --- .../time-entries/pages/time-entries.component.spec.ts | 6 +++--- .../time-entries/pages/time-entries.component.ts | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index 9781720d0..732e7e0bb 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -245,7 +245,7 @@ describe('TimeEntriesComponent', () => { description: 'description', technologies: [], uri: 'abc', - }, shouldRestartEntry: false + }, shouldRestartEntry: true }; component.entryId = 'new-entry'; spyOn(injectedToastrService, 'error'); @@ -265,7 +265,7 @@ describe('TimeEntriesComponent', () => { description: 'description', technologies: [], uri: 'abc', - }, shouldRestartEntry: false + }, shouldRestartEntry: true }; component.entryId = 'new-entry'; spyOn(injectedToastrService, 'error'); @@ -361,7 +361,7 @@ describe('TimeEntriesComponent', () => { technologies: [], uri: 'abc', - }, shouldRestartEntry: true + }, shouldRestartEntry: false }; component.entryId = '123'; spyOn(store, 'dispatch'); diff --git a/src/app/modules/time-entries/pages/time-entries.component.ts b/src/app/modules/time-entries/pages/time-entries.component.ts index 3c2ef721f..1cbab5ebc 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -104,17 +104,18 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { } saveEntry(event: SaveEntryEvent): void { - if (this.activeTimeEntry) { + + if (this.activeTimeEntry && this.entryId === this.activeTimeEntry.id) { const startDateAsLocalDate = new Date(event.entry.start_date); const endDateAsLocalDate = new Date(event.entry.end_date); const activeEntryAsLocalDate = new Date(this.activeTimeEntry.start_date); - const isEditingEntryEqualToActiveEntry = this.entryId === this.activeTimeEntry.id; const isStartDateGreaterThanActiveEntry = startDateAsLocalDate > activeEntryAsLocalDate; const isEndDateGreaterThanActiveEntry = endDateAsLocalDate > activeEntryAsLocalDate; const isTimeEntryOverlapping = isStartDateGreaterThanActiveEntry || isEndDateGreaterThanActiveEntry; - if (!isEditingEntryEqualToActiveEntry && isTimeEntryOverlapping) { + + if (isTimeEntryOverlapping) { this.toastrService.error('You are on the clock and this entry overlaps it, try with earlier times.'); - } else { + } else { this.doSave(event); } } else { @@ -145,7 +146,7 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { if (this.entryId) { event.entry.id = this.entryId; this.store.dispatch(new entryActions.UpdateEntry(event.entry)); - if (event.shouldRestartEntry) { + if (event.shouldRestartEntry && this.entryId === this.activeTimeEntry.id) { this.store.dispatch(new entryActions.RestartEntry(event.entry)); } } else { From 9f275b2809585c9019865ae4a0e09fab67535a3b Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Tue, 19 Jan 2021 17:59:44 -0500 Subject: [PATCH 6/7] fix: TT-117 two entries in progress bug --- .../time-entries/pages/time-entries.component.spec.ts | 11 +++++++---- .../time-entries/pages/time-entries.component.ts | 3 --- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index 732e7e0bb..f8dcaad24 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -247,7 +247,7 @@ describe('TimeEntriesComponent', () => { uri: 'abc', }, shouldRestartEntry: true }; - component.entryId = 'new-entry'; + component.entryId = 'entry_1'; spyOn(injectedToastrService, 'error'); component.saveEntry(newEntry); @@ -257,6 +257,7 @@ describe('TimeEntriesComponent', () => { it('displays an error when end date of entry is greater than active entry start date', async () => { component.activeTimeEntry = entry; + const newEntry = { entry: { project_id: 'p-id', @@ -267,7 +268,7 @@ describe('TimeEntriesComponent', () => { uri: 'abc', }, shouldRestartEntry: true }; - component.entryId = 'new-entry'; + component.entryId = 'entry_1'; spyOn(injectedToastrService, 'error'); component.saveEntry(newEntry); @@ -349,8 +350,9 @@ describe('TimeEntriesComponent', () => { expect(component.doSave).toHaveBeenCalledWith(entryToSave); })); - it('when event contains should restart as true, then a restart Entry action should be triggered', () => { + it('when event contains should update as true, then a restart Entry action should be triggered', () => { component.entry = { start_date: new Date(), id: '1234', technologies: [], project_name: 'time-tracker' }; + const entryToSave = { entry: { id: '123', @@ -363,12 +365,13 @@ describe('TimeEntriesComponent', () => { }, shouldRestartEntry: false }; + component.entryId = '123'; spyOn(store, 'dispatch'); component.doSave(entryToSave); - expect(store.dispatch).toHaveBeenCalledWith(new entryActions.RestartEntry(entryToSave.entry)); + expect(store.dispatch).toHaveBeenCalledWith(new entryActions.UpdateEntry(entryToSave.entry)); }); it('should preload data of last entry when a project is selected while creating new entry ', waitForAsync(() => { diff --git a/src/app/modules/time-entries/pages/time-entries.component.ts b/src/app/modules/time-entries/pages/time-entries.component.ts index 1cbab5ebc..2bc71fa86 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -146,9 +146,6 @@ export class TimeEntriesComponent implements OnInit, OnDestroy { if (this.entryId) { event.entry.id = this.entryId; this.store.dispatch(new entryActions.UpdateEntry(event.entry)); - if (event.shouldRestartEntry && this.entryId === this.activeTimeEntry.id) { - this.store.dispatch(new entryActions.RestartEntry(event.entry)); - } } else { this.store.dispatch(new entryActions.CreateEntry(event.entry)); } From 4de39db7308ff53946b1a0f90fd5ac40088bfeac Mon Sep 17 00:00:00 2001 From: Sandro Castillo Date: Wed, 20 Jan 2021 12:43:01 -0500 Subject: [PATCH 7/7] fix: TT-117 update original values to shouldRestartEntry --- .../time-entries/pages/time-entries.component.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index f8dcaad24..dcd21f2bc 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -245,7 +245,7 @@ describe('TimeEntriesComponent', () => { description: 'description', technologies: [], uri: 'abc', - }, shouldRestartEntry: true + }, shouldRestartEntry: false }; component.entryId = 'entry_1'; spyOn(injectedToastrService, 'error'); @@ -257,7 +257,6 @@ describe('TimeEntriesComponent', () => { it('displays an error when end date of entry is greater than active entry start date', async () => { component.activeTimeEntry = entry; - const newEntry = { entry: { project_id: 'p-id', @@ -266,7 +265,7 @@ describe('TimeEntriesComponent', () => { description: 'description', technologies: [], uri: 'abc', - }, shouldRestartEntry: true + }, shouldRestartEntry: false }; component.entryId = 'entry_1'; spyOn(injectedToastrService, 'error'); @@ -363,7 +362,7 @@ describe('TimeEntriesComponent', () => { technologies: [], uri: 'abc', - }, shouldRestartEntry: false + }, shouldRestartEntry: true }; component.entryId = '123';