diff --git a/package-lock.json b/package-lock.json index 3769fae6d..d02d1bdaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.31.19", + "version": "1.32.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -417,6 +417,28 @@ } } }, + "@angular/cdk": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-11.2.3.tgz", + "integrity": "sha512-QehBMt36nNfXZ8nBGJLLUQlexzQv6rohQmEYXULHarAC3Ily8DnB9wDGJ4emlKyGz1MIVYR0tZP39RmQp0hH+g==", + "requires": { + "parse5": "^5.0.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "optional": true + }, + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" + } + } + }, "@angular/cli": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-10.2.0.tgz", @@ -4601,6 +4623,16 @@ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "bl": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", @@ -7048,24 +7080,24 @@ "dev": true }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true } } @@ -7781,6 +7813,13 @@ "schema-utils": "^2.6.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "file-url": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/file-url/-/file-url-3.0.0.tgz", @@ -20849,6 +20888,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -21418,6 +21458,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, diff --git a/package.json b/package.json index 0c39bb33d..cf057c486 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.31.19", + "version": "1.32.1", "scripts": { "preinstall": "npx npm-force-resolutions", "ng": "ng", @@ -15,6 +15,7 @@ "private": true, "dependencies": { "@angular/animations": "^10.2.2", + "@angular/cdk": "^11.2.3", "@angular/common": "~10.2.2", "@angular/compiler": "~10.2.2", "@angular/core": "~10.2.2", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 505a1d050..755dcd5da 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -10,6 +10,7 @@ import { DataTablesModule } from 'angular-datatables'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { DragDropModule } from '@angular/cdk/drag-drop'; import { NgxPaginationModule } from 'ngx-pagination'; import { AutocompleteLibModule } from 'angular-ng-autocomplete'; @@ -144,6 +145,7 @@ const maskConfig: Partial = { AutocompleteLibModule, NgxMaterialTimepickerModule, UiSwitchModule, + DragDropModule, StoreModule.forRoot(reducers, { metaReducers, }), diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html index 3a6501c3c..2c45f080c 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.html @@ -4,7 +4,7 @@ datatable [dtTrigger]="dtTrigger" [dtOptions]="dtOptions" - *ngIf="(reportDataSource$ | async) as dataSource"> + *ngIf="reportDataSource$ | async as dataSource"> ID @@ -41,7 +41,13 @@ {{ entry.customer_name }} {{ entry.customer_id }} {{ entry.activity_name }} - {{ entry.uri }} + + + + {{ entry.uri }} + + + {{ entry.description }} {{ entry.technologies }} diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss index 1cc15655e..82fab2e2a 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss @@ -1,3 +1,4 @@ +@import '../../../../../styles/colors.scss'; .col{ white-space: nowrap; overflow: hidden; @@ -31,4 +32,26 @@ overflow-x: scroll; width: 100%; display: grid; -} \ No newline at end of file +} + +$url-base-color: $primary; +$url-hover-color: darken($url-base-color, 20); + +.is-url { + cursor: pointer; + color: $url-base-color; + text-decoration: underline; + + &:visited { + color: $dark; + } + + &:hover { + color: $url-hover-color; + } + + &:active { + color: $url-base-color; + } +} + diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts index b58b13050..4bccd5679 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts @@ -21,7 +21,7 @@ describe('Reports Page', () => { description: 'any comment', uri: 'custom uri', project_id: '123', - project_name: 'Time-Tracker' + project_name: 'Time-Tracker', }; const state: EntryState = { @@ -33,38 +33,41 @@ describe('Reports Page', () => { timeEntriesSummary: null, timeEntriesDataSource: { data: [timeEntry], - isLoading: false + isLoading: false, }, reportDataSource: { data: [timeEntry], - isLoading: false - } + isLoading: false, + }, }; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [], - declarations: [TimeEntriesTableComponent, SubstractDatePipe], - providers: [provideMockStore({ initialState: state })], - }).compileComponents(); - store = TestBed.inject(MockStore); - - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [TimeEntriesTableComponent, SubstractDatePipe], + providers: [provideMockStore({ initialState: state })], + }).compileComponents(); + store = TestBed.inject(MockStore); + }) + ); - beforeEach(waitForAsync(() => { - fixture = TestBed.createComponent(TimeEntriesTableComponent); - component = fixture.componentInstance; - store.setState(state); - getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource); - fixture.detectChanges(); - })); + beforeEach( + waitForAsync(() => { + fixture = TestBed.createComponent(TimeEntriesTableComponent); + component = fixture.componentInstance; + store.setState(state); + getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource); + fixture.detectChanges(); + }) + ); it('component should be created', async () => { expect(component).toBeTruthy(); }); it('on success load time entries, the report should be populated', () => { - component.reportDataSource$.subscribe(ds => { + component.reportDataSource$.subscribe((ds) => { expect(ds.data).toEqual(state.reportDataSource.data); }); }); @@ -76,6 +79,34 @@ describe('Reports Page', () => { expect(component.dtTrigger.next).toHaveBeenCalled(); }); + it('when the uri starts with http or https it should return true and open the url in a new tab', () => { + const url = 'http://customuri.com'; + spyOn(component, 'isURL').and.returnValue(true); + spyOn(window, 'open'); + + expect(component.openURLInNewTab(url)).not.toEqual(''); + expect(window.open).toHaveBeenCalledWith(url, '_blank'); + }); + + it('when the uri starts without http or https it should return false and not navigate or open a new tab', () => { + const uriExpected = timeEntry.uri; + spyOn(component, 'isURL').and.returnValue(false); + + expect(component.openURLInNewTab(uriExpected)).toEqual(''); + }); + + const params = [ + {url: 'http://example.com', expected_value: true, with: 'with'}, + {url: 'https://example.com', expected_value: true, with: 'with'}, + {url: 'no-url-example', expected_value: false, with: 'without'} + ]; + params.map((param) => { + it(`when the url starts ${param.with} http or https it should return ${param.expected_value}`, () => { + + expect(component.isURL(param.url)).toEqual(param.expected_value); + }); + }); + afterEach(() => { fixture.destroy(); }); diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index cacf54688..893af0ac1 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -1,19 +1,18 @@ import { formatDate } from '@angular/common'; -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild, NgModule } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { DataTableDirective } from 'angular-datatables'; import * as moment from 'moment'; import { Observable, Subject } from 'rxjs'; import { Entry } from 'src/app/modules/shared/models'; import { DataSource } from 'src/app/modules/shared/models/data-source.model'; - import { EntryState } from '../../../time-clock/store/entry.reducer'; import { getReportDataSource } from '../../../time-clock/store/entry.selectors'; @Component({ selector: 'app-time-entries-table', templateUrl: './time-entries-table.component.html', - styleUrls: ['./time-entries-table.component.scss'] + styleUrls: ['./time-entries-table.component.scss'], }) export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit { dtOptions: any = { @@ -24,7 +23,6 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn { extend: 'colvis', columns: ':not(.hidden-col)', - }, 'print', { @@ -32,30 +30,26 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn exportOptions: { format: { body: (data, row, column, node) => { - return column === 3 ? - moment.duration(data).asHours().toFixed(4).slice(0, -1) : - data; - } - } + return column === 3 ? moment.duration(data).asHours().toFixed(4).slice(0, -1) : data; + }, + }, }, text: 'Excel', - filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` + filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`, }, { extend: 'csv', exportOptions: { format: { body: (data, row, column, node) => { - return column === 3 ? - moment.duration(data).asHours().toFixed(4).slice(0, -1) : - data; - } - } + return column === 3 ? moment.duration(data).asHours().toFixed(4).slice(0, -1) : data; + }, + }, }, text: 'CSV', - filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` - } - ] + filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`, + }, + ], }; dtTrigger: Subject = new Subject(); @ViewChild(DataTableDirective, { static: false }) @@ -91,4 +85,13 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn this.dtTrigger.next(); } } + + openURLInNewTab(uri: string): WindowProxy | string { + return this.isURL(uri) ? window.open(uri, '_blank') : ''; + } + + isURL(uri: string) { + const regex = new RegExp('http*', 'g'); + return regex.test(uri) ? true : false; + } } diff --git a/src/app/modules/time-clock/services/entry.service.spec.ts b/src/app/modules/time-clock/services/entry.service.spec.ts index ca2630120..230e86a32 100644 --- a/src/app/modules/time-clock/services/entry.service.spec.ts +++ b/src/app/modules/time-clock/services/entry.service.spec.ts @@ -143,10 +143,13 @@ describe('EntryService', () => { it('entries are found by project id with a limit 2 by default', () => { const projectId = 'project-id'; + const startDate = (moment().subtract(1, 'months')).format(); + const endDate = moment().format(); service.findEntriesByProjectId(projectId).subscribe(); - const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}?limit=2&project_id=${projectId}`); + const restartEntryRequest = httpMock.expectOne( `${service.baseUrl}?limit=2&project_id=${projectId}&start_date=${startDate}&end_date=${endDate}`); expect(restartEntryRequest.request.method).toBe('GET'); }); + }); diff --git a/src/app/modules/time-clock/services/entry.service.ts b/src/app/modules/time-clock/services/entry.service.ts index 2506b4a5b..5c8a9b28c 100644 --- a/src/app/modules/time-clock/services/entry.service.ts +++ b/src/app/modules/time-clock/services/entry.service.ts @@ -7,6 +7,7 @@ import { environment } from './../../../../environments/environment'; import { TimeEntriesTimeRange } from '../models/time-entries-time-range'; import { DatePipe } from '@angular/common'; import { Entry } from '../../shared/models'; +import * as moment from 'moment'; @Injectable({ providedIn: 'root', @@ -59,7 +60,9 @@ export class EntryService { } findEntriesByProjectId(projectId: string): Observable { - const findEntriesByProjectURL = `${this.baseUrl}?limit=2&project_id=${projectId}`; + const startDate = this.getDateLastMonth(); + const endDate = this.getCurrentDate(); + const findEntriesByProjectURL = `${this.baseUrl}?limit=2&project_id=${projectId}&start_date=${startDate}&end_date=${endDate}`; return this.http.get(findEntriesByProjectURL); } @@ -77,4 +80,12 @@ export class EntryService { } ); } + + getDateLastMonth() { + return (moment().subtract(1, 'months')).format(); + } + + getCurrentDate() { + return moment().format(); + } } diff --git a/src/app/modules/time-entries/pages/time-entries.component.html b/src/app/modules/time-entries/pages/time-entries.component.html index 9df5c7f0a..dd3963053 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.html +++ b/src/app/modules/time-entries/pages/time-entries.component.html @@ -61,8 +61,8 @@