diff --git a/.github/workflows/CD-time-tracker-ui.yml b/.github/workflows/CD-time-tracker-ui.yml index 6c5715aff..2f2a7c666 100644 --- a/.github/workflows/CD-time-tracker-ui.yml +++ b/.github/workflows/CD-time-tracker-ui.yml @@ -29,6 +29,8 @@ jobs: SCOPES: ${{ secrets.scopes }} CLIENT_ID: ${{ secrets.client_id }} AUTHORITY: ${{ secrets.authority }} + STACK_EXCHANGE_ID: ${{ secrets.stack_exchange_id }} + STACK_EXCHANGE_ACCESS_TOKEN: ${{ secrets.stack_exchange_access_token }} run: | chmod +x ./scripts/populate-keys.sh sh ./scripts/populate-keys.sh diff --git a/.github/workflows/CI-mutation-tests.yml b/.github/workflows/CI-mutation-tests.yml index c1ee543a6..264dfe2a5 100644 --- a/.github/workflows/CI-mutation-tests.yml +++ b/.github/workflows/CI-mutation-tests.yml @@ -26,6 +26,8 @@ jobs: SCOPES: ${{ secrets.scopes }} CLIENT_ID: ${{ secrets.client_id }} AUTHORITY: ${{ secrets.authority }} + STACK_EXCHANGE_ID: ${{ secrets.stack_exchange_id }} + STACK_EXCHANGE_ACCESS_TOKEN: ${{ secrets.stack_exchange_access_token }} run: sudo sh scripts/populate-keys.sh - name: Run the test diff --git a/.github/workflows/CI-time-tracker-ui.yml b/.github/workflows/CI-time-tracker-ui.yml index d6e26e465..df3f2c3fa 100644 --- a/.github/workflows/CI-time-tracker-ui.yml +++ b/.github/workflows/CI-time-tracker-ui.yml @@ -44,12 +44,14 @@ jobs: SCOPES: ${{ secrets.scopes }} CLIENT_ID: ${{ secrets.client_id }} AUTHORITY: ${{ secrets.authority }} + STACK_EXCHANGE_ID: ${{ secrets.stack_exchange_id }} + STACK_EXCHANGE_ACCESS_TOKEN: ${{ secrets.stack_exchange_access_token }} run: sudo sh scripts/populate-keys.sh - name: Run the test run: npm run ci-test --if-present - name: Generate coverage report - env: + env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md index f1c637de0..7abe9e04a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Create a file keys.ts with the content pinned in our slack channel: export const AUTHORITY = 'XXX'; export const CLIENT_ID = 'XXX'; export const SCOPES = ['XXX']; +export const STACK_EXCHANGE_ID = 'XXX'; +export const STACK_EXCHANGE_ACCESS_TOKEN = 'XXX'; ``` ### Prepare your environment for vscode diff --git a/scripts/populate-keys.sh b/scripts/populate-keys.sh index e07b44428..614ff3e38 100644 --- a/scripts/populate-keys.sh +++ b/scripts/populate-keys.sh @@ -4,4 +4,6 @@ echo 'export const AUTHORITY = "'$AUTHORITY'";' >> src/environments/keys.ts echo 'export const CLIENT_ID = "'$CLIENT_ID'";' >> src/environments/keys.ts echo 'export const SCOPES = ["'$SCOPES'"];' >> src/environments/keys.ts +echo 'export const STACK_EXCHANGE_ID = "'$STACK_EXCHANGE_ID'";' >> src/environments/keys.ts +echo 'export const STACK_EXCHANGE_ACCESS_TOKEN = "'$STACK_EXCHANGE_ACCESS_TOKEN'";' >> src/environments/keys.ts cat src/environments/keys.ts diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1c8664b38..89e7d1029 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -32,6 +32,7 @@ import { HomeComponent } from './modules/home/home.component'; import { LoginComponent } from './modules/login/login.component'; import { ActivityEffects } from './modules/activities-management/store/activity-management.effects'; import { ProjectEffects } from './modules/project-management/store/project.effects'; +import { TechnologyEffects } from './modules/shared/store/technology.effects'; import { reducers, metaReducers } from './reducers'; import { environment } from '../environments/environment'; import { CustomerComponent } from './modules/customer-managment/pages/customer.component'; @@ -99,7 +100,7 @@ import { CreateProjectTypeComponent } from './modules/customer-managment/compone maxAge: 15, // Retains last 15 states }) : [], - EffectsModule.forRoot([ProjectEffects, ActivityEffects]), + EffectsModule.forRoot([ProjectEffects, ActivityEffects, TechnologyEffects]), ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.html b/src/app/modules/shared/components/details-fields/details-fields.component.html index 66a747528..a1d5012ae 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.html +++ b/src/app/modules/shared/components/details-fields/details-fields.component.html @@ -36,18 +36,38 @@ aria-describedby="inputGroup-sizing-sm" /> -
+ +
Technology
+ +
LOADING...
+
+
+ {{ item.name }} +
+
+
+
+ {{ technology }} + +
+
+
diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.scss b/src/app/modules/shared/components/details-fields/details-fields.component.scss index 2735a1709..2f1cca316 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.scss +++ b/src/app/modules/shared/components/details-fields/details-fields.component.scss @@ -1,4 +1,14 @@ @import '../../../../../styles/colors.scss'; + +@mixin tagTechnology() { + background-color: $modal-button-secondary; + color: #ffffff; + + &:hover { + opacity: 0.8; + } +} + .span-width { width: 6rem; background-image: $background-pantone; @@ -18,3 +28,44 @@ background-color: $modal-button-secondary; color: white; } + +.technology-content { + box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08); + margin: 0 0 2rem 6rem; + max-height: 7.5rem; + overflow-y: auto; + + .technology-list { + cursor: pointer; + font-size: 0.8rem; + margin-bottom: 0.1rem; + padding: 0.2rem 0.5rem; + + &:hover { + opacity: 0.7; + } + } + + .active { + background-color: #efefef; + } +} + +.tags-content { + margin: 2rem 0; + + div { + @include tagTechnology(); + + border-radius: 0.2rem; + box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.75); + font-size: 0.8rem; + padding: 0.1rem 1rem 0.2rem 1.5rem; + position: relative; + margin: 0 0.5rem 0.5rem 0; + + i { + cursor: pointer; + } + } +} diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts index b854e6794..77a8e6093 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.spec.ts @@ -1,35 +1,47 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TechnologyState } from '../../store/technology.reducers'; +import { allTechnologies } from '../../store/technology.selectors'; import { DetailsFieldsComponent } from './details-fields.component'; +import { FilterProjectPipe } from '../../../shared/pipes'; +import * as actions from '../../store/technology.actions'; describe('DetailsFieldsComponent', () => { let component: DetailsFieldsComponent; let fixture: ComponentFixture; + let store: MockStore; + let mockTechnologySelector; + let length; + + const state = { + technologyList: { items: [{ name: 'java' }] }, + isLoading: false, + }; + const initialData = { project: '', activity: '', ticket: '', - technology: '', - comments: '' + comments: '', }; const newData = { project: 'Ernst&Young', activity: 'development', ticket: 'WA-15', - technology: 'Angular', - comments: 'No notes' + comments: 'No notes', }; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [DetailsFieldsComponent], - imports: [ - FormsModule, - ReactiveFormsModule - ], + declarations: [DetailsFieldsComponent, FilterProjectPipe], + providers: [provideMockStore({ initialState: state })], + imports: [FormsModule, ReactiveFormsModule], }).compileComponents(); + store = TestBed.inject(MockStore); + mockTechnologySelector = store.overrideSelector(allTechnologies, state); })); beforeEach(() => { @@ -42,12 +54,6 @@ describe('DetailsFieldsComponent', () => { expect(component).toBeTruthy(); }); - it('should emit saveEntry event', () => { - spyOn(component.saveEntry, 'emit'); - component.onSubmit(); - expect(component.saveEntry.emit).toHaveBeenCalledWith(initialData); - }); - it('should emit ngOnChange without data', () => { component.entryToEdit = null; component.ngOnChanges(); @@ -59,4 +65,73 @@ describe('DetailsFieldsComponent', () => { component.ngOnChanges(); expect(component.entryForm.value).toEqual(newData); }); + + it('should dispatch FindTechnology action #getTechnologies', () => { + const value = 'java'; + spyOn(store, 'dispatch'); + length = value.length; + component.getTechnologies(value); + + expect(store.dispatch).toHaveBeenCalledWith(new actions.FindTechnology(value)); + }); + + it('should NOT dispatch FindTechnology action #getTechnologies', () => { + const value = 'j'; + spyOn(store, 'dispatch'); + length = value.length; + component.getTechnologies(value); + + expect(store.dispatch).not.toHaveBeenCalledWith(new actions.FindTechnology(value)); + }); + + it('should add a new tag #setTechnology', () => { + const name = 'ngrx'; + component.selectedTechnology = ['java', 'javascript']; + component.selectedTechnology.indexOf(name); + length = component.selectedTechnology.length; + component.setTechnology(name); + expect(component.selectedTechnology.length).toBe(3); + }); + + it('should NOT add a new tag #setTechnology', () => { + const name = 'ngrx'; + component.selectedTechnology = [ + 'java', + 'javascript', + 'angular', + 'angular-ui', + 'typescript', + 'scss', + 'bootstrap', + 'jasmine', + 'karme', + 'github', + ]; + component.selectedTechnology.indexOf(name); + length = component.selectedTechnology.length; + component.setTechnology(name); + expect(component.selectedTechnology.length).toBe(10); + }); + + it('should call the removeTag function #setTechnology', () => { + const name = 'java'; + component.selectedTechnology = ['java', 'javascript']; + const index = component.selectedTechnology.indexOf(name); + spyOn(component, 'removeTag'); + component.setTechnology(name); + expect(component.removeTag).toHaveBeenCalledWith(index); + }); + + it('should call the removeTag() function #removeTag', () => { + const index = 1; + component.selectedTechnology = ['java', 'angular']; + component.removeTag(index); + expect(component.selectedTechnology.length).toBe(1); + }); + + it('should emit saveEntry event', () => { + spyOn(component.saveEntry, 'emit'); + component.onSubmit(); + expect(component.saveEntry.emit).toHaveBeenCalledWith(initialData); + }); }); diff --git a/src/app/modules/shared/components/details-fields/details-fields.component.ts b/src/app/modules/shared/components/details-fields/details-fields.component.ts index 159328b3d..0d7592cd5 100644 --- a/src/app/modules/shared/components/details-fields/details-fields.component.ts +++ b/src/app/modules/shared/components/details-fields/details-fields.component.ts @@ -1,48 +1,75 @@ -import { - Component, - OnChanges, - Input, - Output, - EventEmitter, - ViewChild, - ElementRef -} from '@angular/core'; +import { Component, OnChanges, OnInit, Input, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; +import { Store, select } from '@ngrx/store'; +import * as actions from '../../store/technology.actions'; + +import { allTechnologies } from '../../store/technology.selectors'; +import { Technology } from '../../models'; @Component({ selector: 'app-details-fields', templateUrl: './details-fields.component.html', - styleUrls: ['./details-fields.component.scss'] + styleUrls: ['./details-fields.component.scss'], }) -export class DetailsFieldsComponent implements OnChanges { +export class DetailsFieldsComponent implements OnChanges, OnInit { @Input() entryToEdit; @Input() formType: string; @Output() saveEntry = new EventEmitter(); @ViewChild('closeModal') closeModal: ElementRef; entryForm: FormGroup; + technology: Technology; + selectedTechnology: string[] = []; + isLoading = false; - constructor(private formBuilder: FormBuilder) { + constructor(private formBuilder: FormBuilder, private store: Store) { this.entryForm = this.formBuilder.group({ project: '', activity: '', ticket: '', - technology: '', - comments: '' + comments: '', + }); + } + + ngOnInit(): void { + const technologies$ = this.store.pipe(select(allTechnologies)); + technologies$.subscribe((response) => { + this.isLoading = response.isLoading; + this.technology = response.technologyList; }); } ngOnChanges(): void { if (this.entryToEdit) { + this.selectedTechnology = this.entryToEdit.technologies; + this.entryForm.setValue({ project: this.entryToEdit.project, activity: this.entryToEdit.activity, ticket: this.entryToEdit.ticket, - technology: this.entryToEdit.technology, - comments: this.entryToEdit.comments + comments: this.entryToEdit.comments, }); } } + getTechnologies(value) { + if (value.length >= 2) { + this.store.dispatch(new actions.FindTechnology(value)); + } + } + + setTechnology(name: string) { + const index = this.selectedTechnology.indexOf(name); + if (index > -1) { + this.removeTag(index); + } else if (this.selectedTechnology.length < 10) { + this.selectedTechnology = [...this.selectedTechnology, name]; + } + } + + removeTag(index) { + this.selectedTechnology.splice(index, 1); + } + onSubmit() { this.saveEntry.emit(this.entryForm.value); this.closeModal.nativeElement.click(); diff --git a/src/app/modules/shared/components/modal/modal.component.spec.ts b/src/app/modules/shared/components/modal/modal.component.spec.ts index 2380cf7c0..f74b800c8 100644 --- a/src/app/modules/shared/components/modal/modal.component.spec.ts +++ b/src/app/modules/shared/components/modal/modal.component.spec.ts @@ -33,7 +33,7 @@ describe('ModalComponent', () => { startDate: '2020-02-05T15:36:15.887Z', endDate: '2020-02-05T18:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['Angular', 'TypeScript'], }; spyOn(component.removeList, 'emit'); diff --git a/src/app/modules/shared/models/entry.model.ts b/src/app/modules/shared/models/entry.model.ts index a52f04c15..74c642fec 100644 --- a/src/app/modules/shared/models/entry.model.ts +++ b/src/app/modules/shared/models/entry.model.ts @@ -4,7 +4,7 @@ export interface Entry { startDate: string; endDate: string; activity: string; - technology: string; + technologies: string[]; comments?: string; ticket?: string; } diff --git a/src/app/modules/shared/models/index.ts b/src/app/modules/shared/models/index.ts index c16dcfc43..7f6b3d186 100644 --- a/src/app/modules/shared/models/index.ts +++ b/src/app/modules/shared/models/index.ts @@ -1,3 +1,4 @@ export * from './activity.model'; export * from './entry.model'; export * from './project.model'; +export * from './technology.model'; diff --git a/src/app/modules/shared/models/technology.model.ts b/src/app/modules/shared/models/technology.model.ts new file mode 100644 index 000000000..9d4b79686 --- /dev/null +++ b/src/app/modules/shared/models/technology.model.ts @@ -0,0 +1,3 @@ +export interface Technology { + items: Array; +} diff --git a/src/app/modules/shared/services/.gitkeep b/src/app/modules/shared/services/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/modules/shared/services/technology.service.spec.ts b/src/app/modules/shared/services/technology.service.spec.ts new file mode 100644 index 000000000..4632f0430 --- /dev/null +++ b/src/app/modules/shared/services/technology.service.spec.ts @@ -0,0 +1,45 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { Technology } from '../../shared/models'; +import { TechnologyService } from './technology.service'; +import { STACK_EXCHANGE_ID, STACK_EXCHANGE_ACCESS_TOKEN } from '../../../../environments/environment'; + +describe('TechnologyService', () => { + let service: TechnologyService; + let httpMock: HttpTestingController; + + const technologyList: Technology = { items: [{ name: 'java' }, { name: 'javascript' }] }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + service = TestBed.inject(TechnologyService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should create', inject( + [HttpClientTestingModule, TechnologyService], + (httpClient: HttpClientTestingModule, apiService: TechnologyService) => { + expect(apiService).toBeTruthy(); + expect(httpClient).toBeTruthy(); + } + )); + + it('technologies are read using GET from url', () => { + const technologyFoundSize = technologyList.items.length; + const technologyName = 'java'; + service.getTechnologies(technologyName).subscribe((technologyInResponse) => { + expect(technologyInResponse.items.length).toBe(technologyFoundSize); + }); + const getTechnologiesRequest = httpMock.expectOne( + `${service.baseUrl}&inname=${technologyName}&site=stackoverflow&key=${STACK_EXCHANGE_ID}&access_token=${STACK_EXCHANGE_ACCESS_TOKEN}` + ); + expect(getTechnologiesRequest.request.method).toBe('GET'); + getTechnologiesRequest.flush(technologyList); + }); +}); diff --git a/src/app/modules/shared/services/technology.service.ts b/src/app/modules/shared/services/technology.service.ts new file mode 100644 index 000000000..5e2dd88dd --- /dev/null +++ b/src/app/modules/shared/services/technology.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { Technology } from '../models/technology.model'; +import { STACK_EXCHANGE_ID, STACK_EXCHANGE_ACCESS_TOKEN, environment } from '../../../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class TechnologyService { + baseUrl = `${environment.stackexchangeApiUrl}/2.2/tags?order=desc&sort=popular`; + constructor(private http: HttpClient) {} + + getTechnologies(value: string): Observable { + const url = `${this.baseUrl}&inname=${value}&site=stackoverflow&key=${STACK_EXCHANGE_ID}&access_token=${STACK_EXCHANGE_ACCESS_TOKEN}`; + return this.http.get(url); + } +} diff --git a/src/app/modules/shared/store/technology.actions.spec.ts b/src/app/modules/shared/store/technology.actions.spec.ts new file mode 100644 index 000000000..96e02b14a --- /dev/null +++ b/src/app/modules/shared/store/technology.actions.spec.ts @@ -0,0 +1,15 @@ +import * as actions from './technology.actions'; +import { Technology } from '../models'; + +describe('Actions for Technology', () => { + it('FindTechnologySuccess type is TechnologyActionTypes.FIND_TECHNOLOGIES_SUCESS', () => { + const technologyList: Technology = { items: [{ name: 'java' }, { name: 'javascript' }] }; + const findTechnologySuccess = new actions.FindTechnologySuccess(technologyList); + expect(findTechnologySuccess.type).toEqual(actions.TechnologyActionTypes.FIND_TECHNOLOGIES_SUCESS); + }); + + it('FindTechnologyFail type is TechnologyActionTypes.FIND_TECHNOLOGIES_FAIL', () => { + const findTechnologyFail = new actions.FindTechnologyFail('error'); + expect(findTechnologyFail.type).toEqual(actions.TechnologyActionTypes.FIND_TECHNOLOGIES_FAIL); + }); +}); diff --git a/src/app/modules/shared/store/technology.actions.ts b/src/app/modules/shared/store/technology.actions.ts new file mode 100644 index 000000000..a2fb2a415 --- /dev/null +++ b/src/app/modules/shared/store/technology.actions.ts @@ -0,0 +1,28 @@ +import { Action } from '@ngrx/store'; +import { Technology } from '../models/technology.model'; + +export enum TechnologyActionTypes { + FIND_TECHNOLOGIES = '[Technology] FIND_TECHNOLOGIES', + FIND_TECHNOLOGIES_SUCESS = '[Technology] FIND_TECHNOLOGIES_SUCESS', + FIND_TECHNOLOGIES_FAIL = '[Technology] FIND_TECHNOLOGIES_FAIL ', +} + +export class FindTechnology implements Action { + public readonly type = TechnologyActionTypes.FIND_TECHNOLOGIES; + + constructor(readonly payload: string) {} +} + +export class FindTechnologySuccess implements Action { + readonly type = TechnologyActionTypes.FIND_TECHNOLOGIES_SUCESS; + + constructor(readonly payload: Technology) {} +} + +export class FindTechnologyFail implements Action { + public readonly type = TechnologyActionTypes.FIND_TECHNOLOGIES_FAIL; + + constructor(public error: string) {} +} + +export type TechnologyActions = FindTechnology | FindTechnologySuccess | FindTechnologyFail; diff --git a/src/app/modules/shared/store/technology.effects.ts b/src/app/modules/shared/store/technology.effects.ts new file mode 100644 index 000000000..6231e8488 --- /dev/null +++ b/src/app/modules/shared/store/technology.effects.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { ofType, Actions, Effect } from '@ngrx/effects'; +import { Action } from '@ngrx/store'; +import { of, Observable } from 'rxjs'; +import { catchError, map, mergeMap } from 'rxjs/operators'; +import { TechnologyService } from '../services/technology.service'; +import * as actions from './technology.actions'; + +@Injectable() +export class TechnologyEffects { + constructor(private actions$: Actions, private technologyService: TechnologyService) {} + + @Effect() + findTechnology$: Observable = this.actions$.pipe( + ofType(actions.TechnologyActionTypes.FIND_TECHNOLOGIES), + map((action: actions.FindTechnology) => action.payload), + mergeMap((value) => + this.technologyService.getTechnologies(value).pipe( + map((technology) => { + return new actions.FindTechnologySuccess(technology); + }), + catchError((error) => of(new actions.FindTechnologyFail(error))) + ) + ) + ); +} diff --git a/src/app/modules/shared/store/technology.reducers.spec.ts b/src/app/modules/shared/store/technology.reducers.spec.ts new file mode 100644 index 000000000..ac274e163 --- /dev/null +++ b/src/app/modules/shared/store/technology.reducers.spec.ts @@ -0,0 +1,26 @@ +import * as actions from './technology.actions'; +import { technologyReducer, TechnologyState } from './technology.reducers'; +import { Technology } from '../models/technology.model'; + +describe('technologyReducer', () => { + const initialState: TechnologyState = { technologyList: null, isLoading: false }; + + it('on FindTechnology, isLoading is true', () => { + const action = new actions.FindTechnology('java'); + const state = technologyReducer(initialState, action); + expect(state.isLoading).toEqual(true); + }); + + it('on FindTechnologySuccess, technologiesFound are saved in the store', () => { + const technologiesFound: Technology = { items: [{ name: 'java' }] }; + const action = new actions.FindTechnologySuccess(technologiesFound); + const state = technologyReducer(initialState, action); + expect(state.technologyList).toEqual(technologiesFound); + }); + + it('on FindTechnologyFail, technologyList equal []', () => { + const action = new actions.FindTechnologyFail('error'); + const state = technologyReducer(initialState, action); + expect(state.technologyList).toEqual([]); + }); +}); diff --git a/src/app/modules/shared/store/technology.reducers.ts b/src/app/modules/shared/store/technology.reducers.ts new file mode 100644 index 000000000..4fc1c68c6 --- /dev/null +++ b/src/app/modules/shared/store/technology.reducers.ts @@ -0,0 +1,40 @@ +import { TechnologyActions, TechnologyActionTypes } from './technology.actions'; +import { Technology } from '../models/technology.model'; + +export interface TechnologyState { + technologyList: Technology; + isLoading: boolean; +} + +export const initialState = { + technologyList: null, + isLoading: false, +}; + +export const technologyReducer = (state: TechnologyState = initialState, action: TechnologyActions) => { + switch (action.type) { + case TechnologyActionTypes.FIND_TECHNOLOGIES: { + return { + ...state, + isLoading: true, + }; + } + + case TechnologyActionTypes.FIND_TECHNOLOGIES_SUCESS: + return { + ...state, + technologyList: action.payload, + isLoading: false, + }; + + case TechnologyActionTypes.FIND_TECHNOLOGIES_FAIL: { + return { + technologyList: [], + isLoading: false, + }; + } + + default: + return state; + } +}; diff --git a/src/app/modules/shared/store/technology.selectors.ts b/src/app/modules/shared/store/technology.selectors.ts new file mode 100644 index 000000000..0eee15cef --- /dev/null +++ b/src/app/modules/shared/store/technology.selectors.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +import { TechnologyState } from './technology.reducers'; + +const getTechnologyState = createFeatureSelector('technologies'); + +export const allTechnologies = createSelector(getTechnologyState, (state: TechnologyState) => { + return state; +}); 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 52dc47cf9..1f722640f 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 @@ -1,4 +1,5 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { @@ -8,18 +9,38 @@ import { ModalComponent, } from '../../shared/components'; import { GroupByDatePipe } from '../../shared/pipes'; +import { TechnologyState } from '../../shared/store/technology.reducers'; +import { allTechnologies } from '../../shared/store/technology.selectors'; import { TimeEntriesComponent } from './time-entries.component'; +import { FilterProjectPipe } from '../../shared/pipes'; describe('TimeEntriesComponent', () => { let component: TimeEntriesComponent; let fixture: ComponentFixture; + let store: MockStore; + let mockTechnologySelector; + + const state = { + projects: { + projectList: [{ id: 'id', name: 'name', description: 'description' }], + isLoading: false, + }, + activities: { + data: [{ id: 'id', name: 'name', description: 'description' }], + isLoading: false, + message: 'message', + }, + technologyList: { items: [{ name: 'test' }] }, + isLoading: false, + }; + const entry = { id: 'entry_1', project: 'Mido - 05 de Febrero', startDate: '2020-02-05T15:36:15.887Z', endDate: '2020-02-05T18:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['Angular', 'TypeScript'], comments: 'No comments', ticket: 'EY-25', }; @@ -29,13 +50,17 @@ describe('TimeEntriesComponent', () => { declarations: [ EmptyStateComponent, DetailsFieldsComponent, + FilterProjectPipe, GroupByDatePipe, ModalComponent, MonthPickerComponent, TimeEntriesComponent, ], + providers: [provideMockStore({ initialState: state })], imports: [FormsModule, ReactiveFormsModule], }).compileComponents(); + store = TestBed.inject(MockStore); + mockTechnologySelector = store.overrideSelector(allTechnologies, state); })); beforeEach(() => { @@ -66,7 +91,7 @@ describe('TimeEntriesComponent', () => { expect(component.entry.startDate).toBe(entry.startDate); expect(component.entry.endDate).toBe(entry.endDate); expect(component.entry.activity).toBe(entry.activity); - expect(component.entry.technology).toBe(entry.technology); + expect(component.entry.technologies).toEqual(entry.technologies); }); it('should save an Entry', () => { @@ -76,7 +101,6 @@ describe('TimeEntriesComponent', () => { expect(component.entryList[0].startDate).toBe('2020-02-05T15:36:15.887Z'); expect(component.entryList[0].endDate).toBe('2020-02-05T18:36:15.887Z'); expect(component.entryList[0].activity).toBe('development'); - expect(component.entryList[0].technology).toBe('Angular, TypeScript'); }); it('should delete a Entry', () => { 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 e78076580..a99be1d24 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.ts @@ -4,10 +4,9 @@ import { Entry } from '../../shared/models'; @Component({ selector: 'app-time-entries', templateUrl: './time-entries.component.html', - styleUrls: ['./time-entries.component.scss'] + styleUrls: ['./time-entries.component.scss'], }) export class TimeEntriesComponent implements OnInit { - showModal = false; entryId: string; entry: Entry; @@ -21,9 +20,9 @@ export class TimeEntriesComponent implements OnInit { startDate: '2020-02-05T15:36:15.887Z', endDate: '2020-02-05T18:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['Angular', 'TypeScript'], comments: 'No comments', - ticket: 'EY-25' + ticket: 'EY-25', }, { id: 'entry_2', @@ -31,9 +30,9 @@ export class TimeEntriesComponent implements OnInit { startDate: '2020-03-15T20:36:15.887Z', endDate: '2020-03-15T23:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['Angular', 'TypeScript'], comments: 'No comments', - ticket: 'EY-38' + ticket: 'EY-38', }, { id: 'entry_3', @@ -41,9 +40,9 @@ export class TimeEntriesComponent implements OnInit { startDate: '2020-03-15T23:36:15.887Z', endDate: '2020-03-16T05:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['Angular', 'TypeScript'], comments: 'No comments', - ticket: 'EY-225' + ticket: 'EY-225', }, { id: 'entry_4', @@ -51,9 +50,9 @@ export class TimeEntriesComponent implements OnInit { startDate: '2020-03-16T15:36:15.887Z', endDate: '2020-03-16T18:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['javascript', 'java-stream'], comments: 'No comments', - ticket: 'EY-89' + ticket: 'EY-89', }, { id: 'entry_5', @@ -61,16 +60,16 @@ export class TimeEntriesComponent implements OnInit { startDate: '2020-04-01T09:36:15.887Z', endDate: '2020-04-01T15:36:15.887Z', activity: 'development', - technology: 'Angular, TypeScript', + technologies: ['javascript', 'java', 'java-stream'], comments: 'No comments', - ticket: 'EY-59' - } + ticket: 'EY-59', + }, ]; - constructor() { } + constructor() {} ngOnInit(): void { - this.dataByMonth = this.entryList.filter(entry => new Date(entry.startDate).getMonth() === new Date().getMonth()); + this.dataByMonth = this.entryList.filter((entry) => new Date(entry.startDate).getMonth() === new Date().getMonth()); } openModal(itemToDelete: Entry) { @@ -84,10 +83,9 @@ export class TimeEntriesComponent implements OnInit { } saveEntry(newData): void { - const entryIndex = this.entryList.findIndex((entry => entry.id === this.entryId)); + const entryIndex = this.entryList.findIndex((entry) => entry.id === this.entryId); this.entryList[entryIndex].project = newData.project; this.entryList[entryIndex].activity = newData.activity; - this.entryList[entryIndex].technology = newData.technology; this.entryList[entryIndex].ticket = newData.jiraTicket; this.entryList[entryIndex].comments = newData.notes; } @@ -97,6 +95,6 @@ export class TimeEntriesComponent implements OnInit { } getMonth(month: number) { - this.dataByMonth = this.entryList.filter(entry => new Date(entry.startDate).getMonth() === month); + this.dataByMonth = this.entryList.filter((entry) => new Date(entry.startDate).getMonth() === month); } } diff --git a/src/app/reducers/index.ts b/src/app/reducers/index.ts index 65d8047dd..916b37e55 100644 --- a/src/app/reducers/index.ts +++ b/src/app/reducers/index.ts @@ -1,16 +1,19 @@ import { ActionReducerMap, MetaReducer } from '@ngrx/store'; import { projectReducer } from '../modules/project-management/store/project.reducer'; import { activityManagementReducer } from '../modules/activities-management/store/activity-management.reducers'; +import { technologyReducer } from '../modules/shared/store/technology.reducers'; import { environment } from '../../environments/environment'; export interface State { projects; activities; + technologies; } export const reducers: ActionReducerMap = { projects: projectReducer, activities: activityManagementReducer, + technologies: technologyReducer, }; export const metaReducers: MetaReducer[] = !environment.production ? [] : []; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index dd273f6b9..e7bfb6f4f 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -6,12 +6,15 @@ import * as keys from './keys'; export const environment = { production: false, timeTrackerApiUrl: 'https://tsheets-apim.azure-api.net', + stackexchangeApiUrl: 'https://api.stackexchange.com', }; export const AUTHORITY = keys.AUTHORITY; export const CLIENT_ID = keys.CLIENT_ID; export const SCOPES = keys.SCOPES; export const ITEMS_PER_PAGE = 5; +export const STACK_EXCHANGE_ID = keys.STACK_EXCHANGE_ID; +export const STACK_EXCHANGE_ACCESS_TOKEN = keys.STACK_EXCHANGE_ACCESS_TOKEN; /* * For easier debugging in development mode, you can import the following file diff --git a/src/environments/keys.ts b/src/environments/keys.ts index ebdd42371..cddd4b75a 100644 --- a/src/environments/keys.ts +++ b/src/environments/keys.ts @@ -1,3 +1,5 @@ export const AUTHORITY = 'XXX'; export const CLIENT_ID = 'XXX'; export const SCOPES = ['XXX']; +export const STACK_EXCHANGE_ID = 'XXX'; +export const STACK_EXCHANGE_ACCESS_TOKEN = 'XXX';