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) => {