diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts index 4764ac6a8..907d6f656 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts @@ -1,3 +1,4 @@ +import { Observable, of } from 'rxjs'; import { LoadActiveEntry, EntryActionTypes, UpdateEntry } from './../../store/entry.actions'; import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions'; import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; @@ -15,6 +16,7 @@ import { formatDate } from '@angular/common'; import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker'; import * as moment from 'moment'; import { DATE_FORMAT_YEAR } from 'src/environments/environment'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; describe('EntryFieldsComponent', () => { type Merged = TechnologyState & ProjectState; @@ -24,6 +26,7 @@ describe('EntryFieldsComponent', () => { let mockTechnologySelector; let mockProjectsSelector; let entryForm; + let featureManagerService: FeatureManagerService; const actionSub: ActionsSubject = new ActionsSubject(); const toastrServiceStub = { error: (message?: string, title?: string, override?: Partial) => { }, @@ -114,6 +117,7 @@ describe('EntryFieldsComponent', () => { entryForm = TestBed.inject(FormBuilder); mockTechnologySelector = store.overrideSelector(allTechnologies, state.technologies); mockProjectsSelector = store.overrideSelector(getCustomerProjects, state.projects); + featureManagerService = TestBed.inject(FeatureManagerService); })); beforeEach(() => { @@ -136,8 +140,6 @@ describe('EntryFieldsComponent', () => { spyOn(component.entryForm, 'patchValue'); - component.setDataToUpdate(entry); - expect(component.entryForm.patchValue).toHaveBeenCalledTimes(1); expect(component.entryForm.patchValue).toHaveBeenCalledWith( { @@ -151,11 +153,22 @@ describe('EntryFieldsComponent', () => { expect(component.selectedTechnologies).toEqual([]); }); + const exponentialGrowth = [true, false]; + exponentialGrowth.map((toggleValue) => { + it(`when FeatureToggle is ${toggleValue} should return true`, () => { + spyOn(featureManagerService, 'isToggleEnabled').and.returnValue(of(toggleValue)); + const isFeatureToggleActivated: Promise = component.isFeatureToggleActivated(); + expect(featureManagerService.isToggleEnabled).toHaveBeenCalled(); + isFeatureToggleActivated.then((value) => expect(value).toEqual(toggleValue)); + }); + }); + it('displays error message when the date selected is in the future', () => { const mockEntry = { ...entry, start_date : moment().format(DATE_FORMAT_YEAR), start_hour : moment().format('HH:mm') }; + component.newData = mockEntry; component.activeEntry = mockEntry ; component.setDataToUpdate(mockEntry); @@ -411,5 +424,4 @@ describe('EntryFieldsComponent', () => { expect(component.selectedTechnologies).toBe(initialTechnologies); }); - }); diff --git a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts index f6160a48f..c4f361226 100644 --- a/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts +++ b/src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts @@ -1,13 +1,14 @@ import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions'; import { EntryActionTypes, LoadActiveEntry } from './../../store/entry.actions'; -import { filter } from 'rxjs/operators'; -import { Component, OnInit } from '@angular/core'; +import { filter, map } from 'rxjs/operators'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { Store, ActionsSubject, select } from '@ngrx/store'; import { Activity, NewEntry } from '../../../shared/models'; import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; import { TechnologyState } from '../../../shared/store/technology.reducers'; import { ActivityState, LoadActivities } from '../../../activities-management/store'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; import * as entryActions from '../../store/entry.actions'; import { get } from 'lodash'; @@ -16,6 +17,7 @@ import { ToastrService } from 'ngx-toastr'; import { formatDate } from '@angular/common'; import { getTimeEntriesDataSource } from '../../store/entry.selectors'; import { DATE_FORMAT } from 'src/environments/environment'; +import { Subscription } from 'rxjs'; type Merged = TechnologyState & ProjectState & ActivityState; @@ -24,7 +26,7 @@ type Merged = TechnologyState & ProjectState & ActivityState; templateUrl: './entry-fields.component.html', styleUrls: ['./entry-fields.component.scss'], }) -export class EntryFieldsComponent implements OnInit { +export class EntryFieldsComponent implements OnInit, OnDestroy { entryForm: FormGroup; selectedTechnologies: string[] = []; activities: Activity[] = []; @@ -32,8 +34,17 @@ export class EntryFieldsComponent implements OnInit { newData; lastEntry; showTimeInbuttons = false; + loadActivitiesSubscribe: Subscription; + loadActiveEntrySubscribe: Subscription; + actionSetDateSubscribe: Subscription; + loadActivitiesSubject; + loadActiveEntrySubject; + actionSetDateSubject; + + exponentialGrowth; constructor( + private featureManagerService: FeatureManagerService, private formBuilder: FormBuilder, private store: Store, private actionsSubject$: ActionsSubject, @@ -48,17 +59,20 @@ export class EntryFieldsComponent implements OnInit { }); } - ngOnInit(): void { + + async ngOnInit(): Promise { + this.exponentialGrowth = await this.isFeatureToggleActivated(); this.store.dispatch(new LoadActivities()); this.store.dispatch(new entryActions.LoadEntries(new Date().getMonth() + 1, new Date().getFullYear())); - this.actionsSubject$ + this.loadActivitiesSubject = this.actionsSubject$ .pipe(filter((action: any) => action.type === ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS)) .subscribe((action) => { this.activities = action.payload; this.store.dispatch(new LoadActiveEntry()); }); - - this.actionsSubject$ + // tslint:disable-next-line + this.exponentialGrowth ? this.loadActivitiesSubscribe = this.loadActivitiesSubject : this.loadActivitiesSubject; + this.loadActiveEntrySubject = this.actionsSubject$ .pipe( filter( (action: any) => @@ -76,8 +90,9 @@ export class EntryFieldsComponent implements OnInit { this.store.dispatch(new entryActions.LoadEntriesSummary()); } }); - - this.actionsSubject$ + // tslint:disable-next-line + this.exponentialGrowth ? this.loadActiveEntrySubscribe = this.loadActiveEntrySubject : this.loadActiveEntrySubject; + this.actionSetDateSubject = this.actionsSubject$ .pipe(filter((action: any) => action.type === EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS)) .subscribe((action) => { this.activeEntry = action.payload; @@ -90,17 +105,16 @@ export class EntryFieldsComponent implements OnInit { start_date: this.activeEntry.start_date, start_hour: formatDate(this.activeEntry.start_date, 'HH:mm', 'en'), }; - }); + }); + // tslint:disable-next-line + this.exponentialGrowth ? this.actionSetDateSubscribe = this.actionSetDateSubject : this.actionSetDateSubject; } - get activity_id() { return this.entryForm.get('activity_id'); } - get start_hour() { return this.entryForm.get('start_hour'); } - setDataToUpdate(entryData: NewEntry) { if (entryData) { this.entryForm.patchValue({ @@ -174,4 +188,22 @@ export class EntryFieldsComponent implements OnInit { onTechnologyRemoved($event: string[]) { this.store.dispatch(new entryActions.UpdateEntryRunning({ ...this.newData, technologies: $event })); } + + + ngOnDestroy(): void { + + if (this.exponentialGrowth) { + this.loadActivitiesSubscribe.unsubscribe(); + this.loadActiveEntrySubscribe.unsubscribe(); + this.actionSetDateSubscribe.unsubscribe(); + } + } + + isFeatureToggleActivated() { + return this.featureManagerService.isToggleEnabledForUser('exponential-growth').pipe( + map((enabled) => { + return enabled === true ? true : false; + }) + ).toPromise(); + } } diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts index c7dd88ef4..6940e653e 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts @@ -5,18 +5,20 @@ import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { AutocompleteLibModule } from 'angular-ng-autocomplete'; -import { Subscription } from 'rxjs'; +import { Subscription, of } from 'rxjs'; import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors'; import { FilterProjectPipe } from '../../../shared/pipes'; import { UpdateEntryRunning } from '../../store/entry.actions'; import { ProjectListHoverComponent } from './project-list-hover.component'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; describe('ProjectListHoverComponent', () => { let component: ProjectListHoverComponent; let fixture: ComponentFixture; let store: MockStore; let mockProjectsSelector; + let featureManagerService: FeatureManagerService; const toastrServiceStub = { error: (message?: string, title?: string, override?: Partial) => { } }; @@ -56,6 +58,7 @@ describe('ProjectListHoverComponent', () => { fixture = TestBed.createComponent(ProjectListHoverComponent); component = fixture.componentInstance; fixture.detectChanges(); + featureManagerService = TestBed.inject(FeatureManagerService); }); it('should create', () => { @@ -118,7 +121,15 @@ describe('ProjectListHoverComponent', () => { .toHaveBeenCalledWith({ project_id: 'customer - xyz'}); }); - + const exponentialGrowth = [true, false]; + exponentialGrowth.map((toggleValue) => { + it(`when FeatureToggle is ${toggleValue} should return true`, () => { + spyOn(featureManagerService, 'isToggleEnabled').and.returnValue(of(toggleValue)); + const isFeatureToggleActivated: Promise = component.isFeatureToggleActivated(); + expect(featureManagerService.isToggleEnabled).toHaveBeenCalled(); + isFeatureToggleActivated.then((value) => expect(value).toEqual(toggleValue)); + }); + }); // TODO Fix this test since it is throwing this error // Expected spy dispatch to have been called with: // [CreateEntry({ payload: Object({ project_id: '1', start_date: '2020-07-27T22:30:26.743Z', timezone_offset: 300 }), @@ -140,5 +151,4 @@ describe('ProjectListHoverComponent', () => { // }) // ); // }); - }); diff --git a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts index 6213e3ca7..816150dcb 100644 --- a/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts +++ b/src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts @@ -3,7 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { ActionsSubject, select, Store } from '@ngrx/store'; import { ToastrService } from 'ngx-toastr'; import { Observable, Subscription } from 'rxjs'; -import { delay, filter } from 'rxjs/operators'; +import { delay, filter, map } from 'rxjs/operators'; import { Project } from 'src/app/modules/shared/models'; import * as actions from '../../../customer-management/components/projects/components/store/project.actions'; import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer'; @@ -11,13 +11,13 @@ import * as entryActions from '../../store/entry.actions'; import { getIsLoading, getProjects } from './../../../customer-management/components/projects/components/store/project.selectors'; import { EntryActionTypes } from './../../store/entry.actions'; import { getActiveTimeEntry } from './../../store/entry.selectors'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; @Component({ selector: 'app-project-list-hover', templateUrl: './project-list-hover.component.html', styleUrls: ['./project-list-hover.component.scss'], }) export class ProjectListHoverComponent implements OnInit, OnDestroy { - keyword = 'search_field'; listProjects: Project[] = []; activeEntry; @@ -25,17 +25,26 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { showClockIn: boolean; updateEntrySubscription: Subscription; isLoading$: Observable; + projectsSubscribe: Subscription; + activeEntrySubscribe: Subscription; + projectsSubject; + activeEntrySubject; + exponentialGrowth; - constructor(private formBuilder: FormBuilder, private store: Store, + constructor(private featureManagerService: FeatureManagerService, + private formBuilder: FormBuilder, private store: Store, private actionsSubject$: ActionsSubject, private toastrService: ToastrService) { this.projectsForm = this.formBuilder.group({ project_id: null, }); this.isLoading$ = this.store.pipe(delay(0), select(getIsLoading)); } - ngOnInit(): void { + async ngOnInit(): Promise { + + this.exponentialGrowth = await this.isFeatureToggleActivated(); + this.store.dispatch(new actions.LoadProjects()); const projects$ = this.store.pipe(select(getProjects)); - projects$.subscribe((projects) => { + this.projectsSubject = projects$.subscribe((projects) => { this.listProjects = []; projects.forEach((project) => { const projectWithSearchField = {...project}; @@ -45,7 +54,8 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { ); this.loadActiveTimeEntry(); }); - + // tslint:disable-next-line + this.exponentialGrowth ? this.projectsSubscribe = this.projectsSubject : this.projectsSubject; this.updateEntrySubscription = this.actionsSubject$.pipe( filter((action: any) => ( action.type === EntryActionTypes.UPDATE_ENTRY_SUCCESS @@ -61,7 +71,7 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { loadActiveTimeEntry() { this.store.dispatch(new entryActions.LoadActiveEntry()); const activeEntry$ = this.store.pipe(select(getActiveTimeEntry)); - activeEntry$.subscribe((activeEntry) => { + this.activeEntrySubject = activeEntry$.subscribe((activeEntry) => { this.activeEntry = activeEntry; if (activeEntry) { this.showClockIn = false; @@ -71,8 +81,9 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { this.projectsForm.setValue({ project_id: null }); } }); + // tslint:disable-next-line + this.exponentialGrowth ? this.activeEntrySubscribe = this.activeEntrySubject : this.activeEntrySubject; } - setSelectedProject() { this.listProjects.forEach( (project) => { if (project.id === this.activeEntry.project_id) { @@ -110,6 +121,18 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { + if (this.exponentialGrowth) { + this.projectsSubscribe.unsubscribe(); + this.activeEntrySubscribe.unsubscribe(); + } this.updateEntrySubscription.unsubscribe(); } + + isFeatureToggleActivated() { + return this.featureManagerService.isToggleEnabledForUser('exponential-growth').pipe( + map((enabled) => { + return enabled === true ? true : false; + }) + ).toPromise(); + } } diff --git a/src/app/modules/time-clock/pages/time-clock.component.spec.ts b/src/app/modules/time-clock/pages/time-clock.component.spec.ts index 1fa2476ee..582a114af 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.spec.ts +++ b/src/app/modules/time-clock/pages/time-clock.component.spec.ts @@ -1,3 +1,4 @@ +import { of } from 'rxjs'; import { FormBuilder } from '@angular/forms'; import { StopTimeEntryRunning, EntryActionTypes, LoadEntriesSummary } from './../store/entry.actions'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; @@ -11,12 +12,14 @@ import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; import { ActionsSubject } from '@ngrx/store'; import { EntryFieldsComponent } from '../components/entry-fields/entry-fields.component'; import { ToastrService } from 'ngx-toastr'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; describe('TimeClockComponent', () => { let component: TimeClockComponent; let fixture: ComponentFixture; let store: MockStore; let azureAdB2CService: AzureAdB2CService; + let featureManagerService: FeatureManagerService; const actionSub: ActionsSubject = new ActionsSubject(); let injectedToastrService; @@ -69,6 +72,7 @@ describe('TimeClockComponent', () => { fixture.detectChanges(); azureAdB2CService = TestBed.inject(AzureAdB2CService); injectedToastrService = TestBed.inject(ToastrService); + featureManagerService = TestBed.inject(FeatureManagerService); }); it('should be created', () => { @@ -142,4 +146,14 @@ describe('TimeClockComponent', () => { expect(injectedToastrService.error).toHaveBeenCalled(); }); + + const exponentialGrowth = [true, false]; + exponentialGrowth.map((toggleValue) => { + it(`when FeatureToggle is ${toggleValue} should return true`, () => { + spyOn(featureManagerService, 'isToggleEnabled').and.returnValue(of(toggleValue)); + const isFeatureToggleActivated: Promise = component.isFeatureToggleActivated(); + expect(featureManagerService.isToggleEnabled).toHaveBeenCalled(); + isFeatureToggleActivated.then((value) => expect(value).toEqual(toggleValue)); + }); + }); }); diff --git a/src/app/modules/time-clock/pages/time-clock.component.ts b/src/app/modules/time-clock/pages/time-clock.component.ts index ac57d88aa..0667a84d1 100644 --- a/src/app/modules/time-clock/pages/time-clock.component.ts +++ b/src/app/modules/time-clock/pages/time-clock.component.ts @@ -2,12 +2,15 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ActionsSubject, select, Store } from '@ngrx/store'; import { ToastrService } from 'ngx-toastr'; import { Subscription } from 'rxjs'; -import { filter } from 'rxjs/operators'; + +import { filter, map } from 'rxjs/operators'; +import { threadId } from 'worker_threads'; import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service'; import { EntryFieldsComponent } from '../components/entry-fields/entry-fields.component'; import { Entry } from './../../shared/models/entry.model'; import { EntryActionTypes, LoadEntriesSummary, StopTimeEntryRunning } from './../store/entry.actions'; import { getActiveTimeEntry } from './../store/entry.selectors'; +import { FeatureManagerService } from 'src/app/modules/shared/feature-toggles/feature-toggle-manager.service'; @Component({ selector: 'app-time-clock', templateUrl: './time-clock.component.html', @@ -20,22 +23,27 @@ export class TimeClockComponent implements OnInit, OnDestroy { areFieldsVisible = false; activeTimeEntry: Entry; clockOutSubscription: Subscription; + storeSubscribe: Subscription; + storeSubject; + exponentialGrowth; + constructor( + private featureManagerService: FeatureManagerService, private azureAdB2CService: AzureAdB2CService, private store: Store, private toastrService: ToastrService, private actionsSubject$: ActionsSubject ) {} - ngOnDestroy(): void { - this.clockOutSubscription.unsubscribe(); - } + async ngOnInit(): Promise { + + this.exponentialGrowth = await this.isFeatureToggleActivated(); - ngOnInit() { this.username = this.azureAdB2CService.isLogin() ? this.azureAdB2CService.getName() : ''; - this.store.pipe(select(getActiveTimeEntry)).subscribe((activeTimeEntry) => { + + this.storeSubscribe = this.store.pipe(select(getActiveTimeEntry)).subscribe((activeTimeEntry) => { this.activeTimeEntry = activeTimeEntry; if (this.activeTimeEntry) { this.areFieldsVisible = true; @@ -43,9 +51,10 @@ export class TimeClockComponent implements OnInit, OnDestroy { this.areFieldsVisible = false; } }); + // tslint:disable-next-line + this.exponentialGrowth ? this.storeSubscribe = this.storeSubject : this.storeSubject; this.reloadSummariesOnClockOut(); - } reloadSummariesOnClockOut() { @@ -72,4 +81,22 @@ export class TimeClockComponent implements OnInit, OnDestroy { this.toastrService.error('Activity is required'); } } + + ngOnDestroy(): void { + // tslint:disable-next-line + this.exponentialGrowth && this.storeSubscribe.unsubscribe(); + this.clockOutSubscription.unsubscribe(); + this.storeSubscribe.unsubscribe(); } + +isFeatureToggleActivated() { + return this.featureManagerService.isToggleEnabledForUser('exponential-growth').pipe( + map((enabled) => { + return enabled === true ? true : false; + }) + ).toPromise(); +} + +} + +