Skip to content

Commit 2aeb287

Browse files
authored
Merge pull request #479 from ioet/405-load-my-last-entry
feat: closes #405 load last entry when a project has coincidences closes #478 flaky tests
2 parents 0507238 + 8eb604b commit 2aeb287

File tree

10 files changed

+358
-81
lines changed

10 files changed

+358
-81
lines changed

src/app/modules/shared/models/entry.model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
export interface Entry {
22
running?: boolean;
3-
id: string;
3+
id?: string;
44
start_date: Date;
55
end_date?: Date;
66
activity_id?: string;
7-
technologies: string[];
7+
technologies?: string[];
88
uri?: string;
99
activity_name?: string;
1010
description?: string;
1111
owner_email?: string;
1212

1313
project_id?: string;
14-
project_name: string;
14+
project_name?: string;
1515

1616
customer_id?: string;
1717
customer_name?: string;

src/app/modules/time-clock/components/entry-fields/entry-fields.component.spec.ts

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import { LoadActiveEntry, EntryActionTypes } from './../../store/entry.actions';
2+
import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions';
13
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
24
import {MockStore, provideMockStore} from '@ngrx/store/testing';
3-
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
5+
import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
46

57
import {TechnologyState} from '../../../shared/store/technology.reducers';
68
import {allTechnologies} from '../../../shared/store/technology.selectors';
79
import {EntryFieldsComponent} from './entry-fields.component';
810
import {ProjectState} from '../../../customer-management/components/projects/components/store/project.reducer';
911
import {getCustomerProjects} from '../../../customer-management/components/projects/components/store/project.selectors';
10-
import * as entryActions from '../../store/entry.actions';
12+
import { ActionsSubject } from '@ngrx/store';
1113

1214
describe('EntryFieldsComponent', () => {
1315
type Merged = TechnologyState & ProjectState;
@@ -16,6 +18,8 @@ describe('EntryFieldsComponent', () => {
1618
let store: MockStore<Merged>;
1719
let mockTechnologySelector;
1820
let mockProjectsSelector;
21+
let entryForm;
22+
const actionSub: ActionsSubject = new ActionsSubject();
1923

2024
const state = {
2125
projects: {
@@ -60,10 +64,11 @@ describe('EntryFieldsComponent', () => {
6064
beforeEach(async(() => {
6165
TestBed.configureTestingModule({
6266
declarations: [EntryFieldsComponent],
63-
providers: [provideMockStore({initialState: state})],
67+
providers: [provideMockStore({initialState: state}), { provide: ActionsSubject, useValue: actionSub }],
6468
imports: [FormsModule, ReactiveFormsModule],
6569
}).compileComponents();
6670
store = TestBed.inject(MockStore);
71+
entryForm = TestBed.inject(FormBuilder);
6772
mockTechnologySelector = store.overrideSelector(allTechnologies, state.technologies);
6873
mockProjectsSelector = store.overrideSelector(getCustomerProjects, state.projects);
6974
}));
@@ -96,12 +101,6 @@ describe('EntryFieldsComponent', () => {
96101
expect(component.selectedTechnologies).toEqual([]);
97102
});
98103

99-
it('should dispatch UpdateActiveEntry action #onSubmit', () => {
100-
spyOn(store, 'dispatch');
101-
component.onSubmit();
102-
expect(store.dispatch).toHaveBeenCalledWith(new entryActions.UpdateEntryRunning(entry));
103-
});
104-
105104
it('when a technology is added, then dispatch UpdateActiveEntry', () => {
106105
const addedTechnologies = ['react'];
107106
spyOn(store, 'dispatch');
@@ -120,4 +119,114 @@ describe('EntryFieldsComponent', () => {
120119
expect(store.dispatch).toHaveBeenCalled();
121120

122121
});
122+
123+
it('uses the form to check if is valid or not', () => {
124+
entryForm.valid = false;
125+
126+
const result = component.entryFormIsValidate();
127+
128+
expect(result).toBe(entryForm.valid);
129+
});
130+
131+
it('dispatches an action when onSubmit is called', () => {
132+
spyOn(store, 'dispatch');
133+
134+
component.onSubmit();
135+
136+
expect(store.dispatch).toHaveBeenCalled();
137+
});
138+
139+
it('dispatches an action when onTechnologyRemoved is called', () => {
140+
spyOn(store, 'dispatch');
141+
142+
component.onTechnologyRemoved(['foo']);
143+
144+
expect(store.dispatch).toHaveBeenCalled();
145+
});
146+
147+
148+
it('sets the technologies on the class when entry has technologies', () => {
149+
const entryData = { ...entry, technologies: ['foo']};
150+
151+
component.setDataToUpdate(entryData);
152+
153+
expect(component.selectedTechnologies).toEqual(entryData.technologies);
154+
});
155+
156+
157+
it('activites are populated using the payload of the action', () => {
158+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
159+
const action = {
160+
type: ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS,
161+
payload: [],
162+
};
163+
164+
actionSubject.next(action);
165+
166+
expect(component.activities).toEqual(action.payload);
167+
});
168+
169+
it('LoadActiveEntry is dispatchen after LOAD_ACTIVITIES_SUCCESS', () => {
170+
spyOn(store, 'dispatch');
171+
172+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
173+
const action = {
174+
type: ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS,
175+
payload: [],
176+
};
177+
178+
actionSubject.next(action);
179+
180+
expect(store.dispatch).toHaveBeenCalledWith(new LoadActiveEntry());
181+
});
182+
183+
it('when entry has an end_date null then LoadActiveEntry is dispatched', () => {
184+
spyOn(store, 'dispatch');
185+
186+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
187+
const action = {
188+
type: EntryActionTypes.CREATE_ENTRY_SUCCESS,
189+
payload: {end_date: null},
190+
};
191+
192+
actionSubject.next(action);
193+
194+
expect(store.dispatch).toHaveBeenCalledWith(new LoadActiveEntry());
195+
});
196+
197+
it('when entry has an end_date then nothing is dispatched', () => {
198+
spyOn(store, 'dispatch');
199+
200+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
201+
const action = {
202+
type: EntryActionTypes.CREATE_ENTRY_SUCCESS,
203+
payload: {end_date: new Date()},
204+
};
205+
206+
actionSubject.next(action);
207+
208+
expect(store.dispatch).toHaveBeenCalledTimes(0);
209+
});
210+
211+
it('activeEntry is populated using the payload of LOAD_ACTIVE_ENTRY_SUCCESS', () => {
212+
const actionSubject = TestBed.inject(ActionsSubject) as ActionsSubject;
213+
const action = {
214+
type: EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS,
215+
payload: entry,
216+
};
217+
218+
actionSubject.next(action);
219+
220+
expect(component.activeEntry).toBe(action.payload);
221+
});
222+
223+
it('if entryData is null selectedTechnologies is not modified', () => {
224+
const initialTechnologies = ['foo', 'bar'];
225+
component.selectedTechnologies = initialTechnologies;
226+
227+
component.setDataToUpdate(null);
228+
229+
expect(component.selectedTechnologies).toBe(initialTechnologies);
230+
});
231+
123232
});

src/app/modules/time-clock/components/entry-fields/entry-fields.component.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { getActiveTimeEntry } from './../../store/entry.selectors';
1+
import { ActivityManagementActionTypes } from './../../../activities-management/store/activity-management.actions';
2+
import { EntryActionTypes, LoadActiveEntry } from './../../store/entry.actions';
3+
import { filter } from 'rxjs/operators';
24
import { Component, OnInit } from '@angular/core';
35
import { FormBuilder, FormGroup } from '@angular/forms';
4-
import { select, Store } from '@ngrx/store';
6+
import { Store, ActionsSubject } from '@ngrx/store';
57

68
import { Activity, NewEntry } from '../../../shared/models';
79
import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer';
810
import { TechnologyState } from '../../../shared/store/technology.reducers';
9-
import { ActivityState, allActivities, LoadActivities } from '../../../activities-management/store';
11+
import { ActivityState, LoadActivities } from '../../../activities-management/store';
1012

1113
import * as entryActions from '../../store/entry.actions';
1214

@@ -24,37 +26,44 @@ export class EntryFieldsComponent implements OnInit {
2426
activeEntry;
2527
newData;
2628

27-
constructor(private formBuilder: FormBuilder, private store: Store<Merged>) {
29+
constructor(private formBuilder: FormBuilder, private store: Store<Merged>, private actionsSubject$: ActionsSubject) {
2830
this.entryForm = this.formBuilder.group({
2931
description: '',
3032
uri: '',
31-
activity_id: '-1',
33+
activity_id: '',
3234
});
3335
}
3436

3537
ngOnInit(): void {
3638
this.store.dispatch(new LoadActivities());
37-
const activities$ = this.store.pipe(select(allActivities));
38-
activities$.subscribe((response) => {
39-
this.activities = response;
40-
this.loadActiveEntry();
39+
40+
this.actionsSubject$.pipe(
41+
filter((action: any) => (action.type === ActivityManagementActionTypes.LOAD_ACTIVITIES_SUCCESS))
42+
).subscribe((action) => {
43+
this.activities = action.payload;
44+
this.store.dispatch(new LoadActiveEntry());
4145
});
42-
}
4346

44-
loadActiveEntry() {
45-
const activeEntry$ = this.store.pipe(select(getActiveTimeEntry));
46-
activeEntry$.subscribe((response) => {
47-
if (response) {
48-
this.activeEntry = response;
49-
this.setDataToUpdate(this.activeEntry);
50-
this.newData = {
51-
id: this.activeEntry.id,
52-
project_id: this.activeEntry.project_id,
53-
uri: this.activeEntry.uri,
54-
activity_id: this.activeEntry.activity_id,
55-
};
47+
this.actionsSubject$.pipe(
48+
filter((action: any) => (action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS))
49+
).subscribe((action) => {
50+
if (!action.payload.end_date) {
51+
this.store.dispatch(new LoadActiveEntry());
5652
}
5753
});
54+
55+
this.actionsSubject$.pipe(
56+
filter((action: any) => ( action.type === EntryActionTypes.LOAD_ACTIVE_ENTRY_SUCCESS ))
57+
).subscribe((action) => {
58+
this.activeEntry = action.payload;
59+
this.setDataToUpdate(this.activeEntry);
60+
this.newData = {
61+
id: this.activeEntry.id,
62+
project_id: this.activeEntry.project_id,
63+
uri: this.activeEntry.uri,
64+
activity_id: this.activeEntry.activity_id,
65+
};
66+
});
5867
}
5968

6069
get activity_id() {

src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.spec.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import { HttpClientTestingModule } from '@angular/common/http/testing';
2-
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
1+
import { ToastrService, IndividualConfig } from 'ngx-toastr';
2+
import { SwitchTimeEntry, ClockIn } from './../../store/entry.actions';
33
import { FormBuilder } from '@angular/forms';
4-
import { MockStore, provideMockStore } from '@ngrx/store/testing';
4+
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
5+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
6+
import { HttpClientTestingModule } from '@angular/common/http/testing';
57
import { AutocompleteLibModule } from 'angular-ng-autocomplete';
6-
import { IndividualConfig, ToastrService } from 'ngx-toastr';
78
import { Subscription } from 'rxjs';
89
import { ProjectState } from '../../../customer-management/components/projects/components/store/project.reducer';
910
import { getCustomerProjects } from '../../../customer-management/components/projects/components/store/project.selectors';
1011
import { FilterProjectPipe } from '../../../shared/pipes';
11-
import { CreateEntry, UpdateEntryRunning } from '../../store/entry.actions';
12-
import { SwitchTimeEntry } from './../../store/entry.actions';
12+
import { UpdateEntryRunning } from '../../store/entry.actions';
1313
import { ProjectListHoverComponent } from './project-list-hover.component';
1414

1515
describe('ProjectListHoverComponent', () => {
@@ -68,7 +68,7 @@ describe('ProjectListHoverComponent', () => {
6868

6969
component.clockIn(1, 'customer', 'project');
7070

71-
expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(CreateEntry));
71+
expect(store.dispatch).toHaveBeenCalledWith(jasmine.any(ClockIn));
7272
});
7373

7474
it('dispatch a UpdateEntryRunning action on updateProject', () => {
@@ -118,6 +118,7 @@ describe('ProjectListHoverComponent', () => {
118118
.toHaveBeenCalledWith({ project_id: 'customer - xyz'});
119119
});
120120

121+
121122
// TODO Fix this test since it is throwing this error
122123
// Expected spy dispatch to have been called with:
123124
// [CreateEntry({ payload: Object({ project_id: '1', start_date: '2020-07-27T22:30:26.743Z', timezone_offset: 300 }),

src/app/modules/time-clock/components/project-list-hover/project-list-hover.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class ProjectListHoverComponent implements OnInit, OnDestroy {
8383
start_date: new Date().toISOString(),
8484
timezone_offset: new Date().getTimezoneOffset(),
8585
};
86-
this.store.dispatch(new entryActions.CreateEntry(entry));
86+
this.store.dispatch(new entryActions.ClockIn(entry));
8787
this.projectsForm.setValue( { project_id: `${customerName} - ${name}`, } );
8888
}
8989

src/app/modules/time-clock/services/entry.service.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,13 @@ describe('EntryService', () => {
123123
const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}/${entry}/restart`);
124124
expect(restartEntryRequest.request.method).toBe('POST');
125125
});
126+
127+
it('entries are found by project id with a limit 2 by default', () => {
128+
const projectId = 'project-id';
129+
130+
service.findEntriesByProjectId(projectId).subscribe();
131+
132+
const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}?limit=2&project_id=${projectId}`);
133+
expect(restartEntryRequest.request.method).toBe('GET');
134+
});
126135
});

src/app/modules/time-clock/services/entry.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export class EntryService {
5757
return this.http.get<TimeEntriesSummary>(summaryUrl);
5858
}
5959

60+
findEntriesByProjectId(projectId: string): Observable<Entry[]> {
61+
const findEntriesByProjectURL = `${this.baseUrl}?limit=2&project_id=${projectId}`;
62+
return this.http.get<Entry[]>(findEntriesByProjectURL);
63+
}
64+
6065
loadEntriesByTimeRange(range: TimeEntriesTimeRange, userId: string): Observable<any> {
6166
const MAX_NUMBER_OF_ENTRIES_FOR_REPORTS = 9999;
6267
return this.http.get(this.baseUrl,

src/app/modules/time-clock/store/entry.actions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export enum EntryActionTypes {
1313
LOAD_ENTRIES = '[Entry] LOAD_ENTRIES',
1414
LOAD_ENTRIES_SUCCESS = '[Entry] LOAD_ENTRIES_SUCCESS',
1515
LOAD_ENTRIES_FAIL = '[Entry] LOAD_ENTRIES_FAIL',
16+
CLOCK_IN = '[Entry] CLOCK_IN',
17+
CLOCK_IN_SUCCESS = '[Entry] CLOCK_IN_SUCCESS',
1618
CREATE_ENTRY = '[Entry] CREATE_ENTRY',
1719
CREATE_ENTRY_SUCCESS = '[Entry] CREATE_ENTRY_SUCCESS',
1820
CREATE_ENTRY_FAIL = '[Entry] CREATE_ENTRY_FAIL',
@@ -39,6 +41,16 @@ export enum EntryActionTypes {
3941
RESTART_ENTRY_FAIL = '[Entry] RESTART_ENTRY_FAIL',
4042
}
4143

44+
export class ClockIn implements Action {
45+
public readonly type = EntryActionTypes.CLOCK_IN;
46+
constructor(readonly payload: NewEntry) {}
47+
}
48+
49+
export class ClockInSuccess implements Action {
50+
public readonly type = EntryActionTypes.CLOCK_IN_SUCCESS;
51+
constructor() {}
52+
}
53+
4254
export class SwitchTimeEntry implements Action {
4355
public readonly type = EntryActionTypes.SWITCH_TIME_ENTRY;
4456
constructor(readonly idEntrySwitching: string, readonly idProjectSwitching) {}
@@ -250,6 +262,8 @@ export class RestartEntryFail implements Action {
250262
}
251263

252264
export type EntryActions =
265+
| ClockIn
266+
| ClockInSuccess
253267
| LoadEntriesSummary
254268
| LoadEntriesSummarySuccess
255269
| LoadEntriesSummaryFail

0 commit comments

Comments
 (0)