Skip to content

Commit da18ee9

Browse files
committed
fix: TT-178 Make URI clickable when possible
1 parent 80ccc1f commit da18ee9

File tree

5 files changed

+107
-42
lines changed

5 files changed

+107
-42
lines changed

src/app/modules/reports/components/time-entries-table/time-entries-table.component.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
datatable
55
[dtTrigger]="dtTrigger"
66
[dtOptions]="dtOptions"
7-
*ngIf="(reportDataSource$ | async) as dataSource">
7+
*ngIf="reportDataSource$ | async as dataSource">
88
<thead class="thead-blue">
99
<tr class="d-flex">
1010
<th class="hidden-col">ID</th>
@@ -41,7 +41,13 @@
4141
<td class="col md-col">{{ entry.customer_name }}</td>
4242
<td class="hidden-col">{{ entry.customer_id }}</td>
4343
<td class="col md-col">{{ entry.activity_name }}</td>
44-
<td class="col lg-col">{{ entry.uri }}</td>
44+
<td class="col lg-col">
45+
<ng-container *ngIf="entry.uri !== null">
46+
<a [class.is-url]="isURL(entry.uri)" (click)="openURLInNewTab(entry.uri)">
47+
{{ entry.uri }}
48+
</a>
49+
</ng-container>
50+
</td>
4551
<td class="col lg-col">{{ entry.description }}</td>
4652
<td class="col lg-col">{{ entry.technologies }}</td>
4753
</tr>

src/app/modules/reports/components/time-entries-table/time-entries-table.component.scss

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@import '../../../../../styles/colors.scss';
12
.col{
23
white-space: nowrap;
34
overflow: hidden;
@@ -31,4 +32,26 @@
3132
overflow-x: scroll;
3233
width: 100%;
3334
display: grid;
34-
}
35+
}
36+
37+
$url-base-color: $primary;
38+
$url-hover-color: darken($url-base-color, 20);
39+
40+
.is-url {
41+
cursor: pointer;
42+
color: $url-base-color;
43+
text-decoration: underline;
44+
45+
&:visited {
46+
color: $dark;
47+
}
48+
49+
&:hover {
50+
color: $url-hover-color;
51+
}
52+
53+
&:active {
54+
color: $url-base-color;
55+
}
56+
}
57+

src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('Reports Page', () => {
2121
description: 'any comment',
2222
uri: 'custom uri',
2323
project_id: '123',
24-
project_name: 'Time-Tracker'
24+
project_name: 'Time-Tracker',
2525
};
2626

2727
const state: EntryState = {
@@ -33,38 +33,41 @@ describe('Reports Page', () => {
3333
timeEntriesSummary: null,
3434
timeEntriesDataSource: {
3535
data: [timeEntry],
36-
isLoading: false
36+
isLoading: false,
3737
},
3838
reportDataSource: {
3939
data: [timeEntry],
40-
isLoading: false
41-
}
40+
isLoading: false,
41+
},
4242
};
4343

44-
beforeEach(waitForAsync(() => {
45-
TestBed.configureTestingModule({
46-
imports: [],
47-
declarations: [TimeEntriesTableComponent, SubstractDatePipe],
48-
providers: [provideMockStore({ initialState: state })],
49-
}).compileComponents();
50-
store = TestBed.inject(MockStore);
51-
52-
}));
44+
beforeEach(
45+
waitForAsync(() => {
46+
TestBed.configureTestingModule({
47+
imports: [],
48+
declarations: [TimeEntriesTableComponent, SubstractDatePipe],
49+
providers: [provideMockStore({ initialState: state })],
50+
}).compileComponents();
51+
store = TestBed.inject(MockStore);
52+
})
53+
);
5354

54-
beforeEach(waitForAsync(() => {
55-
fixture = TestBed.createComponent(TimeEntriesTableComponent);
56-
component = fixture.componentInstance;
57-
store.setState(state);
58-
getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource);
59-
fixture.detectChanges();
60-
}));
55+
beforeEach(
56+
waitForAsync(() => {
57+
fixture = TestBed.createComponent(TimeEntriesTableComponent);
58+
component = fixture.componentInstance;
59+
store.setState(state);
60+
getReportDataSourceSelectorMock = store.overrideSelector(getReportDataSource, state.reportDataSource);
61+
fixture.detectChanges();
62+
})
63+
);
6164

6265
it('component should be created', async () => {
6366
expect(component).toBeTruthy();
6467
});
6568

6669
it('on success load time entries, the report should be populated', () => {
67-
component.reportDataSource$.subscribe(ds => {
70+
component.reportDataSource$.subscribe((ds) => {
6871
expect(ds.data).toEqual(state.reportDataSource.data);
6972
});
7073
});
@@ -76,6 +79,34 @@ describe('Reports Page', () => {
7679
expect(component.dtTrigger.next).toHaveBeenCalled();
7780
});
7881

82+
it('when the uri starts with http or https it should return true and open the url in a new tab', () => {
83+
const url = 'http://customuri.com';
84+
spyOn(component, 'isURL').and.returnValue(true);
85+
spyOn(window, 'open');
86+
87+
expect(component.openURLInNewTab(url)).not.toEqual('');
88+
expect(window.open).toHaveBeenCalledWith(url, '_blank');
89+
});
90+
91+
it('when the uri starts without http or https it should return false and not navigate or open a new tab', () => {
92+
const uriExpected = timeEntry.uri;
93+
spyOn(component, 'isURL').and.returnValue(false);
94+
95+
expect(component.openURLInNewTab(uriExpected)).toEqual('');
96+
});
97+
98+
const params = [
99+
{url: 'http://example.com', expected_value: true, with: 'with'},
100+
{url: 'https://example.com', expected_value: true, with: 'with'},
101+
{url: 'no-url-example', expected_value: false, with: 'without'}
102+
];
103+
params.map((param) => {
104+
it(`when the url starts ${param.with} http or https it should return ${param.expected_value}`, () => {
105+
106+
expect(component.isURL(param.url)).toEqual(param.expected_value);
107+
});
108+
});
109+
79110
afterEach(() => {
80111
fixture.destroy();
81112
});

src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import { formatDate } from '@angular/common';
2-
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
2+
import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild, NgModule } from '@angular/core';
33
import { select, Store } from '@ngrx/store';
44
import { DataTableDirective } from 'angular-datatables';
55
import * as moment from 'moment';
66
import { Observable, Subject } from 'rxjs';
77
import { Entry } from 'src/app/modules/shared/models';
88
import { DataSource } from 'src/app/modules/shared/models/data-source.model';
9-
109
import { EntryState } from '../../../time-clock/store/entry.reducer';
1110
import { getReportDataSource } from '../../../time-clock/store/entry.selectors';
1211

1312
@Component({
1413
selector: 'app-time-entries-table',
1514
templateUrl: './time-entries-table.component.html',
16-
styleUrls: ['./time-entries-table.component.scss']
15+
styleUrls: ['./time-entries-table.component.scss'],
1716
})
1817
export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewInit {
1918
dtOptions: any = {
@@ -24,38 +23,33 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn
2423
{
2524
extend: 'colvis',
2625
columns: ':not(.hidden-col)',
27-
2826
},
2927
'print',
3028
{
3129
extend: 'excel',
3230
exportOptions: {
3331
format: {
3432
body: (data, row, column, node) => {
35-
return column === 3 ?
36-
moment.duration(data).asHours().toFixed(4).slice(0, -1) :
37-
data;
38-
}
39-
}
33+
return column === 3 ? moment.duration(data).asHours().toFixed(4).slice(0, -1) : data;
34+
},
35+
},
4036
},
4137
text: 'Excel',
42-
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`
38+
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`,
4339
},
4440
{
4541
extend: 'csv',
4642
exportOptions: {
4743
format: {
4844
body: (data, row, column, node) => {
49-
return column === 3 ?
50-
moment.duration(data).asHours().toFixed(4).slice(0, -1) :
51-
data;
52-
}
53-
}
45+
return column === 3 ? moment.duration(data).asHours().toFixed(4).slice(0, -1) : data;
46+
},
47+
},
5448
},
5549
text: 'CSV',
56-
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`
57-
}
58-
]
50+
filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}`,
51+
},
52+
],
5953
};
6054
dtTrigger: Subject<any> = new Subject();
6155
@ViewChild(DataTableDirective, { static: false })
@@ -91,4 +85,13 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn
9185
this.dtTrigger.next();
9286
}
9387
}
88+
89+
openURLInNewTab(uri: string): WindowProxy | string {
90+
return this.isURL(uri) ? window.open(uri, '_blank') : '';
91+
}
92+
93+
isURL(uri: string) {
94+
const regex = new RegExp('http*', 'g');
95+
return regex.test(uri) ? true : false;
96+
}
9497
}

src/app/modules/time-entries/pages/time-entries.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,13 @@ export class TimeEntriesComponent implements OnInit, OnDestroy {
160160
this.selectedMonthAsText = moment().month(event.monthIndex).format('MMMM');
161161
this.store.dispatch(new entryActions.LoadEntries(this.selectedMonth, this.selectedYear));
162162
}
163+
163164
openModal(item: any) {
164165
this.idToDelete = item.id;
165166
this.message = `Are you sure you want to delete ${item.activity_name}?`;
166167
this.showModal = true;
167168
}
169+
168170
resetDraggablePosition(event: any): void {
169171
event.source._dragRef.reset();
170172
}

0 commit comments

Comments
 (0)