diff --git a/README.md b/README.md
index fd7a90650..94e90975a 100644
--- a/README.md
+++ b/README.md
@@ -134,7 +134,7 @@ In any case, the app will automatically reload if you change anything in the sou
- **test**: Adding missing tests or correcting existing tests.
### Example
- fix: TT-48 implement semantic versioning
+ fix: TTA-48 implement semantic versioning
Prefix to use in the space fix:
`(fix: |feat: |perf: |build: |ci: |docs: |refactor: |style: |test: )`
@@ -146,9 +146,9 @@ In any case, the app will automatically reload if you change anything in the sou
| `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 |
### Branch names format
-For example if your task in Jira is **TT-48 implement semantic versioning** your branch name is:
+For example if your task in Jira is **TTA-48 implement semantic versioning** your branch name is:
```
- TT-48-implement-semantic-versioning
+ TTA-48-implement-semantic-versioning
```
## Code scaffolding
diff --git a/package-lock.json b/package-lock.json
index 4d80a05a8..0463d144d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "time-tracker",
- "version": "1.75.19",
+ "version": "1.75.24",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6409,7 +6409,7 @@
"normalize-path": "^3.0.0",
"p-limit": "^3.0.1",
"schema-utils": "^2.7.0",
- "serialize-javascript": "^4.0.0",
+ "serialize-javascript": "^3.1.0",
"webpack-sources": "^1.4.3"
},
"dependencies": {
@@ -8868,12 +8868,12 @@
}
},
"find-versions": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz",
- "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz",
+ "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==",
"dev": true,
"requires": {
- "semver-regex": "^2.0.0"
+ "semver-regex": "^3.1.2"
}
},
"flatted": {
@@ -9681,43 +9681,53 @@
}
},
"husky": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.3.tgz",
- "integrity": "sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ==",
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz",
+ "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==",
"dev": true,
"requires": {
- "chalk": "^3.0.0",
+ "chalk": "^4.0.0",
"ci-info": "^2.0.0",
- "compare-versions": "^3.5.1",
- "cosmiconfig": "^6.0.0",
- "find-versions": "^3.2.0",
+ "compare-versions": "^3.6.0",
+ "cosmiconfig": "^7.0.0",
+ "find-versions": "^4.0.0",
"opencollective-postinstall": "^2.0.2",
- "pkg-dir": "^4.2.0",
+ "pkg-dir": "^5.0.0",
"please-upgrade-node": "^3.2.0",
"slash": "^3.0.0",
"which-pm-runs": "^1.0.0"
},
"dependencies": {
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
"cosmiconfig": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
- "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
+ "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
"dev": true,
"requires": {
"@types/parse-json": "^4.0.0",
- "import-fresh": "^3.1.0",
+ "import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
- "yaml": "^1.7.2"
+ "yaml": "^1.10.0"
}
},
"find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
- "locate-path": "^5.0.0",
+ "locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
@@ -9732,21 +9742,30 @@
}
},
"locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
- "p-locate": "^4.1.0"
+ "p-locate": "^5.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
}
},
"p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
- "p-limit": "^2.2.0"
+ "p-limit": "^3.0.2"
}
},
"parse-json": {
@@ -9768,12 +9787,12 @@
"dev": true
},
"pkg-dir": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+ "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
"dev": true,
"requires": {
- "find-up": "^4.0.0"
+ "find-up": "^5.0.0"
}
},
"resolve-from": {
@@ -21637,12 +21656,6 @@
"lru-cache": "^6.0.0"
}
},
- "semver-regex": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz",
- "integrity": "sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==",
- "dev": true
- },
"shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -21715,7 +21728,7 @@
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
- "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"dev": true
},
"semver-diff": {
@@ -21768,9 +21781,9 @@
}
},
"semver-regex": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz",
- "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==",
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
+ "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
"dev": true
},
"send": {
@@ -23568,7 +23581,7 @@
"jest-worker": "^26.3.0",
"p-limit": "^3.0.2",
"schema-utils": "^2.6.6",
- "serialize-javascript": "^4.0.0",
+ "serialize-javascript": "^3.1.0",
"source-map": "^0.6.1",
"terser": "^5.0.0",
"webpack-sources": "^1.4.3"
@@ -24869,7 +24882,7 @@
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
- "serialize-javascript": "^4.0.0",
+ "serialize-javascript": "^3.1.0",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
@@ -25631,6 +25644,12 @@
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
},
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true
+ },
"zone.js": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz",
diff --git a/package.json b/package.json
index 83238d243..1ecef220e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "time-tracker",
- "version": "1.75.19",
+ "version": "1.75.24",
"scripts": {
"preinstall": "npx npm-force-resolutions",
"ng": "ng",
@@ -89,7 +89,7 @@
"commit-message-validator": "0.1.11",
"datatables.net": "1.10.22",
"datatables.net-dt": "1.10.21",
- "husky": "4.2.3",
+ "husky": "4.3.8",
"jasmine-core": "3.5.0",
"karma": "6.3.16",
"karma-chrome-launcher": "3.1.0",
@@ -117,7 +117,7 @@
"config": {
"commit-message-validator": {
"pattern": "^(fix: TTA-|feat: TTA-|perf: TTA-|build: TTA-|ci: TTA-|docs: TTA-|refactor: TTA-|style: TTA-|test: TTA-|code-smell: TTA-)[0-9].*",
- "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."
+ "errorMessage": "\nYour commit message must comply with the following pattern:\n ^(fix: TTA-|feat: TTA-|perf: TTA-|build: TTA-|ci: TTA-|docs: TTA-|refactor: TTA-|style: TTA-|test: TTA-|code-smell: TTA-)[0-9].*\n followed by any commit message.\n\n Example:\n fix: TTA-43 any commit message\n"
}
},
"resolutions": {
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 0680b43f9..90c6b6463 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 391675003..a7115745c 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -3,7 +3,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { CommonModule, DatePipe } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
-import { NgModule, Component } from '@angular/core';
+import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { DataTablesModule } from 'angular-datatables';
@@ -14,6 +14,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
@@ -95,6 +97,8 @@ import { SearchUserComponent } from './modules/shared/components/search-user/sea
import { TimeRangeCustomComponent } from './modules/reports/components/time-range-custom/time-range-custom.component';
import { TimeRangeHeaderComponent } from './modules/reports/components/time-range-custom/time-range-header/time-range-header.component';
import { TimeRangeOptionsComponent } from './modules/reports/components/time-range-custom/time-range-options/time-range-options.component';
+import { SpinnerOverlayComponent } from './modules/shared/components/spinner-overlay/spinner-overlay.component';
+import { SpinnerInterceptor } from './modules/shared/interceptors/spinner.interceptor';
const maskConfig: Partial = {
validation: false,
@@ -154,6 +158,7 @@ const maskConfig: Partial = {
TimeRangeCustomComponent,
TimeRangeHeaderComponent,
TimeRangeOptionsComponent,
+ SpinnerOverlayComponent,
],
imports: [
NgxMaskModule.forRoot(maskConfig),
@@ -172,6 +177,7 @@ const maskConfig: Partial = {
DataTablesModule,
AutocompleteLibModule,
NgxMaterialTimepickerModule,
+ MatProgressSpinnerModule,
UiSwitchModule,
DragDropModule,
MatIconModule,
@@ -206,6 +212,11 @@ const maskConfig: Partial = {
useClass: InjectTokenInterceptor,
multi: true,
},
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: SpinnerInterceptor,
+ multi: true,
+ },
DatePipe,
CookieService,
{provide: Window, useValue: window}
diff --git a/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.scss b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.scss
new file mode 100644
index 000000000..af4311aba
--- /dev/null
+++ b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.scss
@@ -0,0 +1,5 @@
+@use "../../../../../styles/colors.scss" as colors;
+
+:host ::ng-deep .mat-progress-spinner circle, .mat-spinner circle {
+ stroke: colors.$warning;
+}
diff --git a/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.spec.ts b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.spec.ts
new file mode 100644
index 000000000..4fe84d202
--- /dev/null
+++ b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SpinnerOverlayComponent } from './spinner-overlay.component';
+
+describe('SpinnerOverlayComponent', () => {
+ let component: SpinnerOverlayComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ SpinnerOverlayComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(SpinnerOverlayComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.ts b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.ts
new file mode 100644
index 000000000..00bca4517
--- /dev/null
+++ b/src/app/modules/shared/components/spinner-overlay/spinner-overlay.component.ts
@@ -0,0 +1,11 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-spinner-overlay',
+ template: '',
+ styleUrls: ['./spinner-overlay.component.scss'],
+})
+
+export class SpinnerOverlayComponent {
+ constructor() {}
+}
diff --git a/src/app/modules/shared/interceptors/spinner.interceptor.spec.ts b/src/app/modules/shared/interceptors/spinner.interceptor.spec.ts
new file mode 100644
index 000000000..aa7d28bb1
--- /dev/null
+++ b/src/app/modules/shared/interceptors/spinner.interceptor.spec.ts
@@ -0,0 +1,49 @@
+
+
+import { Overlay } from '@angular/cdk/overlay';
+import { TestBed } from '@angular/core/testing';
+import { SpinnerInterceptor } from './spinner.interceptor';
+import { HttpHandler, HttpRequest, HttpResponse, HttpEvent } from '@angular/common/http';
+import { Observable, of } from 'rxjs';
+import { SpinnerOverlayService } from '../services/spinner-overlay.service';
+
+
+describe('SpinnerInterceptorService test', () => {
+ TestBed.configureTestingModule({
+ providers: [
+ SpinnerInterceptor,
+ Overlay
+ ],
+ });
+
+ class MockHttpHandler implements HttpHandler {
+ handle(req: HttpRequest): Observable> {
+ return of(new HttpResponse());
+ }
+ }
+
+ let overlay: Overlay;
+ let httpHandler: HttpHandler;
+ let spinnerInterceptor: SpinnerInterceptor;
+
+ beforeEach(() => {
+ overlay = jasmine.createSpyObj('Overlay', ['create']);
+ httpHandler = new MockHttpHandler();
+ spinnerInterceptor = new SpinnerInterceptor(new SpinnerOverlayService(overlay));
+ });
+
+ it('should be created', () => {
+ expect(spinnerInterceptor).toBeTruthy();
+ });
+
+ it('if request is made then spinnerInterceptor is called', () => {
+ const request = new HttpRequest('GET', '/recent');
+ spyOn(spinnerInterceptor, 'intercept');
+
+ spinnerInterceptor.intercept(request, httpHandler);
+
+ expect(spinnerInterceptor.intercept).toHaveBeenCalledWith(request, httpHandler);
+
+ });
+
+});
diff --git a/src/app/modules/shared/interceptors/spinner.interceptor.ts b/src/app/modules/shared/interceptors/spinner.interceptor.ts
new file mode 100644
index 000000000..200628271
--- /dev/null
+++ b/src/app/modules/shared/interceptors/spinner.interceptor.ts
@@ -0,0 +1,30 @@
+import {
+ HttpEvent,
+ HttpHandler,
+ HttpInterceptor,
+ HttpRequest,
+} from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable, Subscription } from 'rxjs';
+import { finalize } from 'rxjs/operators';
+import { SpinnerOverlayService } from '../services/spinner-overlay.service';
+
+@Injectable()
+export class SpinnerInterceptor implements HttpInterceptor {
+ constructor(private readonly spinnerOverlayService: SpinnerOverlayService) {}
+
+ intercept(
+ req: HttpRequest,
+ next: HttpHandler
+ ): Observable> {
+ if(req.url.endsWith('recent')){
+ const spinnerSubscription: Subscription = this.spinnerOverlayService.spinner$.subscribe();
+ return next
+ .handle(req)
+ .pipe(finalize(() => spinnerSubscription.unsubscribe()));
+ }else{
+ return next.handle(req);
+ }
+
+ }
+}
diff --git a/src/app/modules/shared/services/spinner-overlay.service.spec.ts b/src/app/modules/shared/services/spinner-overlay.service.spec.ts
new file mode 100644
index 000000000..cde9934cc
--- /dev/null
+++ b/src/app/modules/shared/services/spinner-overlay.service.spec.ts
@@ -0,0 +1,66 @@
+import { TestBed } from '@angular/core/testing';
+import { Overlay } from '@angular/cdk/overlay';
+
+import { SpinnerOverlayService } from './spinner-overlay.service';
+import { SpinnerInterceptor } from '../interceptors/spinner.interceptor';
+import { HttpEvent, HttpHandler, HttpRequest, HttpResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
+import { Observable, of } from 'rxjs';
+import { ComponentPortal } from 'ngx-toastr';
+
+
+describe('SpinnerOverlayService test', () => {
+
+ class MockHttpHandler extends HttpHandler {
+ handle(req: HttpRequest): Observable> {
+ return of(new HttpResponse());
+ }
+ }
+
+ let spinnerService: SpinnerOverlayService;
+ let spinnerInterceptor: SpinnerInterceptor;
+ let mockHttpHandler: HttpHandler;
+ let overlayRef: any;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [
+ Overlay,
+ SpinnerInterceptor,
+ {
+ provide: HTTP_INTERCEPTORS,
+ useClass: SpinnerInterceptor,
+ multi: true,
+ },
+ ],
+ });
+ spinnerService = TestBed.inject(SpinnerOverlayService);
+ spinnerInterceptor = TestBed.inject(SpinnerInterceptor);
+ mockHttpHandler = new MockHttpHandler();
+ overlayRef = spinnerService.overlayRef;
+ });
+
+ it('should be created', () => {
+ expect(spinnerService).toBeTruthy();
+ });
+
+ it('if request is made then spinnerService is show', () => {
+ const request = new HttpRequest('GET', '/recent');
+ spyOn(spinnerService, 'show');
+
+ spinnerInterceptor.intercept(request, mockHttpHandler);
+
+ expect(spinnerService.show).toHaveBeenCalled();
+ expect(ComponentPortal).toBeTruthy();
+ });
+
+
+ it('if hide calls detach method of overlayRef and then sets it to undefined', () => {
+ spyOn(spinnerService, 'hide');
+
+ spinnerService.hide();
+
+ expect(spinnerService.hide).toHaveBeenCalled();
+ expect(overlayRef).toBeUndefined();
+ });
+
+});
diff --git a/src/app/modules/shared/services/spinner-overlay.service.ts b/src/app/modules/shared/services/spinner-overlay.service.ts
new file mode 100644
index 000000000..1b74a6f27
--- /dev/null
+++ b/src/app/modules/shared/services/spinner-overlay.service.ts
@@ -0,0 +1,44 @@
+import { Overlay, OverlayRef } from '@angular/cdk/overlay';
+import { ComponentPortal } from '@angular/cdk/portal';
+import { Injectable } from '@angular/core';
+import { defer, NEVER } from 'rxjs';
+import { finalize, share } from 'rxjs/operators';
+import { SpinnerOverlayComponent } from './../components/spinner-overlay/spinner-overlay.component';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SpinnerOverlayService {
+ public overlayRef: OverlayRef = undefined;
+ static spinner$: any;
+
+ constructor(private readonly overlay: Overlay) {}
+
+ public readonly spinner$ = defer(() => {
+ this.show();
+ return NEVER.pipe(
+ finalize(() => {
+ this.hide();
+ })
+ );
+ }).pipe(share());
+
+ public show(): void {
+ Promise.resolve(null).then(() => {
+ this.overlayRef = this.overlay.create({
+ positionStrategy: this.overlay
+ .position()
+ .global()
+ .centerHorizontally()
+ .centerVertically(),
+ hasBackdrop: true,
+ });
+ this.overlayRef.attach(new ComponentPortal(SpinnerOverlayComponent));
+ });
+ }
+
+ public hide(): void {
+ this.overlayRef.detach();
+ this.overlayRef = undefined;
+ }
+}
diff --git a/src/app/modules/time-clock/store/entry.effects.ts b/src/app/modules/time-clock/store/entry.effects.ts
index 40c1cb1c9..f9fc9d769 100644
--- a/src/app/modules/time-clock/store/entry.effects.ts
+++ b/src/app/modules/time-clock/store/entry.effects.ts
@@ -45,6 +45,9 @@ export class EntryEffects {
mergeMap(() =>
this.entryService.summary().pipe(
map((response) => {
+ if (!response){
+ this.toastrService.warning('Your summary information could not be loaded');
+ }
return new actions.LoadEntriesSummarySuccess(response);
}),
catchError((error) => {