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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,24 @@ Install the following extensions:
- `Prettier - Code formatter`.
- Go to user settings (`settings.json`) and enable formatting on save: `"editor.formatOnSave": true`.

### Commit message format
A commit message needs to start with one of the following words to bump the application version
properly (This application is following a semver strategy for versioning https://semver.org/)
### Sumary
- **fix** is equal to Patch Release example: 1.0.1
- **feat** is equal to Feature Release example: 1.1.0
- **perf** is equal to Breaking Release example: 2.0.0
### Commit messages format
Commit messages' format follows the [Conventional Commits guidelines](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification,
and specifically we are relying on the [Angular commit specifications](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines) to bump the [semantic version](https://semver.org/) and generate app change log.

Below there are some common examples you can use for your commit messages:

- **feat**: A new feature.
- **fix**: A bug fix.
- **perf**: A code change that improves performance.
- **build**: Changes that affect the build system or external dependencies (example scopes: npm, ts configuration).
- **ci**: Changes to our CI or CD configuration files and scripts (example scopes: Azure devops, github actions).
- **docs**: Documentation only changes.
- **refactor**: A code change that neither fixes a bug nor adds a feature.
- **style**: Changes that do not affect the meaning of the code (typos, white-space, formatting, missing semi-colons, etc).
It is important to mention that this key is not related to css styles.
- **test**: Adding missing tests or correcting existing tests.
### Example
fix: #48 implement semantic version.
fix: #48 implement semantic versioning.

## 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`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
},
"config": {
"commit-message-validator": {
"pattern": "^(fix: #|feat: #|perf: #)[0-9].*",
"pattern": "^(fix: #|feat: #|perf: #|build: #|ci: #|docs: #|refactor: #|style: #|test: #)[0-9].*",
"errorMessage": "Your commit message needs to start with fix:, feat:, or perf: followed by issue number, e.g. fix: #43 any commit message"
}
},
Expand Down
5 changes: 1 addition & 4 deletions src/app/modules/time-clock/pages/time-clock.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
<app-notification [notificationMsg]="message | async" [isError]="isError" *ngIf="showNotification"></app-notification>
<div class="text-center mt-3">
<div class="alert alert-danger" role="alert">
{{message}}
</div>

<div class="card">
<div class="card-body">
<h6 class="text-left"><strong>Summary</strong></h6>
Expand Down
37 changes: 25 additions & 12 deletions src/app/modules/time-clock/pages/time-clock.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { StopTimeEntryRunning } from './../store/entry.actions';
import {EntryActionTypes, StopTimeEntryRunning } from './../store/entry.actions';
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideMockStore, MockStore } from '@ngrx/store/testing';

import { TimeClockComponent } from './time-clock.component';
import { ProjectState } from '../../customer-management/components/projects/components/store/project.reducer';
import { ProjectListHoverComponent } from '../components';
import { ProjectService } from '../../customer-management/components/projects/components/services/project.service';
import { FilterProjectPipe } from '../../shared/pipes';
import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
import {ActionsSubject} from '@ngrx/store';

describe('TimeClockComponent', () => {
let component: TimeClockComponent;
let fixture: ComponentFixture<TimeClockComponent>;
let store: MockStore<ProjectState>;
let projectService: ProjectService;
let azureAdB2CService: AzureAdB2CService;
const actionSub: ActionsSubject = new ActionsSubject();
const state = {
projects: {
projects: [{ id: 'id', name: 'name', project_type_id: '' }],
Expand Down Expand Up @@ -44,7 +44,10 @@ describe('TimeClockComponent', () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [TimeClockComponent, ProjectListHoverComponent, FilterProjectPipe],
providers: [ProjectService, AzureAdB2CService, provideMockStore({ initialState: state })],
providers: [
AzureAdB2CService,
{ provide: ActionsSubject, useValue: actionSub },
provideMockStore({ initialState: state })],
}).compileComponents();
store = TestBed.inject(MockStore);
}));
Expand All @@ -53,7 +56,6 @@ describe('TimeClockComponent', () => {
fixture = TestBed.createComponent(TimeClockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
projectService = TestBed.inject(ProjectService);
azureAdB2CService = TestBed.inject(AzureAdB2CService);
});

Expand All @@ -77,13 +79,6 @@ describe('TimeClockComponent', () => {
expect(azureAdB2CService.getName).toHaveBeenCalledTimes(0);
});

it('Service injected via inject(...) and TestBed.get(...) should be the same instance', inject(
[ProjectService],
(injectService: ProjectService) => {
expect(injectService).toBe(projectService);
}
));

it('clockOut dispatch a StopTimeEntryRunning action', () => {
spyOn(store, 'dispatch');

Expand All @@ -92,4 +87,22 @@ describe('TimeClockComponent', () => {
expect(store.dispatch).toHaveBeenCalledWith(new StopTimeEntryRunning('id'));
});

it('on success create entry, the notification is shown', () => {
const actionSubject = TestBed.get(ActionsSubject) as ActionsSubject;
const action = {
type: EntryActionTypes.CREATE_ENTRY_SUCCESS
};
actionSubject.next(action);
expect(component.showNotification).toEqual(true);
});

it('on success stop entry, the notification is shown', () => {
const actionSubject = TestBed.get(ActionsSubject) as ActionsSubject;
const action = {
type: EntryActionTypes.STOP_TIME_ENTRY_RUNNING_SUCCESS
};
actionSubject.next(action);
expect(component.showNotification).toEqual(true);
});

});
37 changes: 25 additions & 12 deletions src/app/modules/time-clock/pages/time-clock.component.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { getStatusMessage } from './../../customer-management/store/customer-management.selectors';
import { getActiveTimeEntry } from './../store/entry.selectors';
import { StopTimeEntryRunning } from './../store/entry.actions';
import { Entry } from './../../shared/models/entry.model';
import { Store, select } from '@ngrx/store';
import { Component, OnInit } from '@angular/core';
import { AzureAdB2CService } from '../../login/services/azure.ad.b2c.service';
import {getStatusMessage} from './../../time-clock/store/entry.selectors';
import {getActiveTimeEntry} from './../store/entry.selectors';
import {EntryActionTypes, StopTimeEntryRunning} from './../store/entry.actions';
import {Entry} from './../../shared/models/entry.model';
import {Store, select, ActionsSubject} from '@ngrx/store';
import {Component, OnInit} from '@angular/core';
import {AzureAdB2CService} from '../../login/services/azure.ad.b2c.service';
import {Observable, Subscription} from 'rxjs';
import {filter} from 'rxjs/operators';

@Component({
selector: 'app-time-clock',
templateUrl: './time-clock.component.html',
styleUrls: ['./time-clock.component.scss'],
})
export class TimeClockComponent implements OnInit {

username: string;
areFieldsVisible = false;
activeTimeEntry: Entry;
message: string;
message: Observable<string>;
showNotification = false;
isError = false;
actionsSubscription: Subscription;

constructor(private azureAdB2CService: AzureAdB2CService, private store: Store<Entry>) {
constructor(private azureAdB2CService: AzureAdB2CService, private store: Store<Entry>, private actionsSubject$: ActionsSubject) {
}

ngOnInit() {
this.message = this.store.pipe(select(getStatusMessage));
this.username = this.azureAdB2CService.isLogin() ? this.azureAdB2CService.getName() : '';
this.store.pipe(select(getActiveTimeEntry)).subscribe((activeTimeEntry) => {
this.activeTimeEntry = activeTimeEntry;
Expand All @@ -31,8 +36,12 @@ export class TimeClockComponent implements OnInit {
this.areFieldsVisible = false;
}
});
this.store.pipe(select(getStatusMessage)).subscribe((valueMessage) => {
this.message = valueMessage;

this.actionsSubscription = this.actionsSubject$.pipe(
filter((action: any) => (action.type === EntryActionTypes.CREATE_ENTRY_SUCCESS) ||
action.type === EntryActionTypes.STOP_TIME_ENTRY_RUNNING_SUCCESS)
).subscribe((action) => {
this.displayNotification();
});
}

Expand All @@ -41,4 +50,8 @@ export class TimeClockComponent implements OnInit {
this.areFieldsVisible = false;
}

displayNotification() {
this.showNotification = true;
setTimeout(() => ((this.showNotification = false), (this.isError = false)), 3000);
}
}
4 changes: 2 additions & 2 deletions src/app/modules/time-clock/store/entry.actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ describe('Actions for Entries', () => {
project_id: '1',
description: 'It is good for learning',
});
expect(updateActiveEntrySuccess.type).toEqual(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS);
expect(updateActiveEntrySuccess.type).toEqual(actions.EntryActionTypes.UPDATE_ACTIVE_ENTRY_SUCCESS);
});

it('UpdateActiveEntryFail type is EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL', () => {
const updateActiveEntryFail = new actions.UpdateActiveEntryFail('error');
expect(updateActiveEntryFail.type).toEqual(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL);
expect(updateActiveEntryFail.type).toEqual(actions.EntryActionTypes.UPDATE_ACTIVE_ENTRY_FAIL);
});
});
12 changes: 6 additions & 6 deletions src/app/modules/time-clock/store/entry.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ export enum EntryActionTypes {
CREATE_ENTRY = '[Entry] CREATE_ENTRY',
CREATE_ENTRY_SUCCESS = '[Entry] CREATE_ENTRY_SUCCESS',
CREATE_ENTRY_FAIL = '[Entry] CREATE_ENTRY_FAIL',
UDPATE_ACTIVE_ENTRY = '[Entry] UDPATE_ACTIVE_ENTRY',
UDPATE_ACTIVE_ENTRY_SUCCESS = '[Entry] UDPATE_ACTIVE_ENTRY_SUCCESS',
UDPATE_ACTIVE_ENTRY_FAIL = '[Entry] UDPATE_ACTIVE_ENTRY_FAIL',
UPDATE_ACTIVE_ENTRY = '[Entry] UPDATE_ACTIVE_ENTRY',
UPDATE_ACTIVE_ENTRY_SUCCESS = '[Entry] UPDATE_ACTIVE_ENTRY_SUCCESS',
UPDATE_ACTIVE_ENTRY_FAIL = '[Entry] UPDATE_ACTIVE_ENTRY_FAIL',
STOP_TIME_ENTRY_RUNNING = '[Entry] STOP_TIME_ENTRIES_RUNNING',
STOP_TIME_ENTRY_RUNNING_SUCCESS = '[Entry] STOP_TIME_ENTRIES_RUNNING_SUCCESS',
STOP_TIME_ENTRY_RUNNING_FAILED = '[Entry] STOP_TIME_ENTRIES_RUNNING_FAILED',
Expand Down Expand Up @@ -50,19 +50,19 @@ export class CreateEntryFail implements Action {
}

export class UpdateActiveEntry implements Action {
public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY;
public readonly type = EntryActionTypes.UPDATE_ACTIVE_ENTRY;

constructor(public payload: NewEntry) {}
}

export class UpdateActiveEntrySuccess implements Action {
public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS;
public readonly type = EntryActionTypes.UPDATE_ACTIVE_ENTRY_SUCCESS;

constructor(public payload: NewEntry) {}
}

export class UpdateActiveEntryFail implements Action {
public readonly type = EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL;
public readonly type = EntryActionTypes.UPDATE_ACTIVE_ENTRY_FAIL;

constructor(public error: string) {}
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/time-clock/store/entry.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class EntryEffects {

@Effect()
updateActiveEntry$: Observable<Action> = this.actions$.pipe(
ofType(actions.EntryActionTypes.UDPATE_ACTIVE_ENTRY),
ofType(actions.EntryActionTypes.UPDATE_ACTIVE_ENTRY),
map((action: actions.UpdateActiveEntry) => action.payload),
mergeMap((project) =>
this.entryService.updateActiveEntry(project).pipe(
Expand Down
11 changes: 10 additions & 1 deletion src/app/modules/time-clock/store/entry.reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ describe('entryReducer', () => {
expect(state.isLoading).toEqual(true);
});

it('on CreateEntrySuccess, message is updated', () => {
const entryToCreate: NewEntry = { project_id: '1', start_date: '2020-04-21T19:51:36.559000+00:00' };
const action = new actions.CreateEntrySuccess(entryToCreate);
const state = entryReducer(initialState, action);

expect(state.message).toEqual('You clocked-in successfully');
});

it('on CreateEntryFail, entryList equal []', () => {
const action = new actions.CreateEntryFail('error');
const state = entryReducer(initialState, action);
Expand Down Expand Up @@ -81,12 +89,13 @@ describe('entryReducer', () => {
expect(state.isLoading).toEqual(true);
});

it('on StopTimeEntryRunningSuccess, active to be null', () => {
it('on StopTimeEntryRunningSuccess, active is null and message is updated', () => {
const action = new actions.StopTimeEntryRunningSuccess('id');

const state = entryReducer(initialState, action);

expect(state.active).toEqual(null);
expect(state.message).toEqual('You clocked-out successfully');
});

it('on UpdateActiveEntryFail, isLoading is false', () => {
Expand Down
10 changes: 5 additions & 5 deletions src/app/modules/time-clock/store/entry.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi
active: action.payload,
entryList: [...state.entryList, action.payload],
isLoading: false,
message: 'Entry Created',
message: 'You clocked-in successfully',
};
}

Expand All @@ -65,14 +65,14 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi
};
}

case EntryActionTypes.UDPATE_ACTIVE_ENTRY: {
case EntryActionTypes.UPDATE_ACTIVE_ENTRY: {
return {
...state,
isLoading: true,
};
}

case EntryActionTypes.UDPATE_ACTIVE_ENTRY_SUCCESS: {
case EntryActionTypes.UPDATE_ACTIVE_ENTRY_SUCCESS: {
const activeEntry = { ...state.active, ...action.payload };

return {
Expand All @@ -82,7 +82,7 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi
};
}

case EntryActionTypes.UDPATE_ACTIVE_ENTRY_FAIL: {
case EntryActionTypes.UPDATE_ACTIVE_ENTRY_FAIL: {
return {
...state,
active: null,
Expand All @@ -102,7 +102,7 @@ export const entryReducer = (state: EntryState = initialState, action: EntryActi
...state,
active: null,
isLoading: false,
message: 'You just clocked-out successfully',
message: 'You clocked-out successfully',
};
}

Expand Down
10 changes: 10 additions & 0 deletions src/app/modules/time-clock/store/entry.selectors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as selectors from './entry.selectors';

describe('Entry selectors', () => {
it('should select the message', () => {
const anyMessage = 'my-message';
const entryState = { message: anyMessage };

expect(selectors.getStatusMessage.projector(entryState)).toBe(anyMessage);
});
});
2 changes: 2 additions & 0 deletions src/app/modules/time-clock/store/entry.selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ const getEntryState = createFeatureSelector('entries');
export const getActiveTimeEntry = createSelector(getEntryState, (state: EntryState) => {
return state.active;
});

export const getStatusMessage = createSelector(getEntryState, (state: EntryState) => state.message);