Skip to content

Commit ae87f2b

Browse files
fix: external url copy (#2323)
1 parent 2613864 commit ae87f2b

File tree

2 files changed

+117
-37
lines changed

2 files changed

+117
-37
lines changed

projects/components/src/link/link.component.test.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { RouterLinkWithHref } from '@angular/router';
22
import { RouterTestingModule } from '@angular/router/testing';
3-
import { NavigationService, TrackDirective } from '@hypertrace/common';
3+
import {
4+
ExternalNavigationWindowHandling,
5+
NavigationParamsType,
6+
NavigationService,
7+
TrackDirective
8+
} from '@hypertrace/common';
49
import { createHostFactory, mockProvider, SpectatorHost } from '@ngneat/spectator/jest';
510
import { MockDirective } from 'ng-mocks';
611
import { of } from 'rxjs';
@@ -26,10 +31,12 @@ describe('Link component', () => {
2631

2732
const anchorElement = spectator.query('.ht-link');
2833
expect(anchorElement).toExist();
29-
expect(anchorElement).toHaveClass('ht-link disabled');
34+
expect(anchorElement).toHaveClass('ht-link internal disabled');
3035
});
3136

3237
test('Link should navigate correctly to external URLs', () => {
38+
window.open = jest.fn();
39+
3340
spectator = createHost(`<ht-link [paramsOrUrl]="paramsOrUrl"></ht-link>`, {
3441
hostProps: {
3542
paramsOrUrl: 'http://test.hypertrace.ai'
@@ -47,28 +54,30 @@ describe('Link component', () => {
4754
],
4855
extras: { skipLocationChange: true }
4956
})
50-
)
57+
),
58+
isExternalUrl: jest.fn().mockReturnValue(true)
5159
})
5260
]
5361
});
5462

55-
const anchorElement = spectator.query('.ht-link');
63+
let anchorElement = spectator.query('.ht-link.external') as HTMLAnchorElement;
5664
expect(anchorElement).toExist();
5765
expect(anchorElement).not.toHaveClass('disabled');
58-
const routerLinkDirective = spectator.query(RouterLinkWithHref);
59-
60-
expect(routerLinkDirective).toBeDefined();
61-
expect(routerLinkDirective?.routerLink).toEqual([
62-
'/external',
63-
{
64-
url: 'http://test.hypertrace.ai',
65-
navType: 'same_window'
66+
expect(anchorElement.href).toBe('http://test.hypertrace.ai/');
67+
spectator.click(anchorElement);
68+
expect(window.open).toHaveBeenLastCalledWith('http://test.hypertrace.ai', '_self');
69+
70+
// With new window
71+
spectator.setHostInput({
72+
paramsOrUrl: {
73+
navType: NavigationParamsType.External,
74+
url: '/test',
75+
windowHandling: ExternalNavigationWindowHandling.NewWindow
6676
}
67-
]);
68-
expect(routerLinkDirective!.skipLocationChange).toBeTruthy();
69-
expect(routerLinkDirective!.queryParams).toBeUndefined();
70-
expect(routerLinkDirective!.queryParamsHandling).toBeUndefined();
71-
expect(routerLinkDirective!.replaceUrl).toBeUndefined();
77+
});
78+
expect(anchorElement.href).toBe('http://localhost/test');
79+
spectator.click(anchorElement);
80+
expect(window.open).toHaveBeenLastCalledWith('/test', undefined);
7281
});
7382

7483
test('Link should navigate correctly to internal relative URLs', () => {
@@ -83,7 +92,7 @@ describe('Link component', () => {
8392
]
8493
});
8594

86-
const anchorElement = spectator.query('.ht-link');
95+
const anchorElement = spectator.query('.ht-link.internal');
8796
expect(anchorElement).toExist();
8897
expect(anchorElement).not.toHaveClass('disabled');
8998
const routerLinkDirective = spectator.query(RouterLinkWithHref);
@@ -108,7 +117,7 @@ describe('Link component', () => {
108117
]
109118
});
110119

111-
const anchorElement = spectator.query('.ht-link');
120+
const anchorElement = spectator.query('.ht-link.internal');
112121
expect(anchorElement).toExist();
113122
expect(anchorElement).not.toHaveClass('disabled');
114123
const routerLinkDirective = spectator.query(RouterLinkWithHref);
@@ -133,9 +142,9 @@ describe('Link component', () => {
133142
]
134143
});
135144

136-
const anchorElement = spectator.query('.ht-link');
145+
const anchorElement = spectator.query('.ht-link.internal');
137146
expect(anchorElement).toExist();
138-
expect(anchorElement).toHaveClass('ht-link disabled');
147+
expect(anchorElement).toHaveClass('ht-link internal disabled');
139148

140149
const routerLinkDirective = spectator.query(RouterLinkWithHref);
141150

projects/components/src/link/link.component.ts

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
22
import { NavigationExtras } from '@angular/router';
3-
import { NavigationParams, NavigationPath, NavigationService } from '@hypertrace/common';
3+
import {
4+
ExternalNavigationParams,
5+
ExternalNavigationWindowHandling,
6+
NavigationParams,
7+
NavigationParamsType,
8+
NavigationPath,
9+
NavigationService,
10+
assertUnreachable
11+
} from '@hypertrace/common';
412
import { isNil } from 'lodash-es';
513
import { EMPTY, Observable } from 'rxjs';
614

@@ -9,21 +17,40 @@ import { EMPTY, Observable } from 'rxjs';
917
styleUrls: ['./link.component.scss'],
1018
changeDetection: ChangeDetectionStrategy.OnPush,
1119
template: `
12-
<a
13-
*htLetAsync="this.navData$ as navData"
14-
class="ht-link"
15-
[ngClass]="{ disabled: this.disabled || !navData }"
16-
[routerLink]="navData?.path"
17-
[queryParams]="navData?.extras?.queryParams"
18-
[queryParamsHandling]="navData?.extras?.queryParamsHandling"
19-
[skipLocationChange]="navData?.extras?.skipLocationChange"
20-
[replaceUrl]="navData?.extras?.replaceUrl"
21-
[htTrack]
22-
htTrackLabel="{{ navData && navData.path ? navData.path : '' }}"
23-
[attr.aria-label]="this.ariaLabel"
24-
>
25-
<ng-content></ng-content>
26-
</a>
20+
<ng-container *htLetAsync="this.navData$ as navData">
21+
<ng-template #contentHolder>
22+
<ng-content></ng-content>
23+
</ng-template>
24+
25+
<ng-container *ngIf="this.externalNavParams; else internalLinkTemplate">
26+
<a
27+
class="ht-link external"
28+
[ngClass]="{ disabled: this.disabled || !navData }"
29+
[attr.href]="this.externalNavParams.url"
30+
[attr.aria-label]="this.ariaLabel"
31+
(click)="this.onExternalLinkClick($event, this.externalNavParams)"
32+
>
33+
<ng-container *ngTemplateOutlet="contentHolder"></ng-container>
34+
</a>
35+
</ng-container>
36+
37+
<ng-template #internalLinkTemplate>
38+
<a
39+
class="ht-link internal"
40+
[ngClass]="{ disabled: this.disabled || !navData }"
41+
[routerLink]="navData?.path"
42+
[queryParams]="navData?.extras?.queryParams"
43+
[queryParamsHandling]="navData?.extras?.queryParamsHandling"
44+
[skipLocationChange]="navData?.extras?.skipLocationChange"
45+
[replaceUrl]="navData?.extras?.replaceUrl"
46+
[htTrack]
47+
htTrackLabel="{{ navData && navData.path ? navData.path : '' }}"
48+
[attr.aria-label]="this.ariaLabel"
49+
>
50+
<ng-container *ngTemplateOutlet="contentHolder"></ng-container>
51+
</a>
52+
</ng-template>
53+
</ng-container>
2754
`
2855
})
2956
export class LinkComponent implements OnChanges {
@@ -37,12 +64,56 @@ export class LinkComponent implements OnChanges {
3764
public ariaLabel?: string;
3865

3966
public navData$: Observable<NavData> = EMPTY;
67+
public isExternal: boolean = false;
68+
public externalNavParams?: ExternalNavigationParams;
4069

4170
public constructor(private readonly navigationService: NavigationService) {}
4271

4372
public ngOnChanges(): void {
73+
this.externalNavParams = this.checkAndBuildExternalNavParams();
4474
this.navData$ = isNil(this.paramsOrUrl) ? EMPTY : this.navigationService.buildNavigationParams$(this.paramsOrUrl);
4575
}
76+
77+
public onExternalLinkClick(event: MouseEvent, externalNavParams: ExternalNavigationParams): void {
78+
event.preventDefault();
79+
window.open(externalNavParams.url, this.asWindowName(externalNavParams.windowHandling));
80+
}
81+
82+
private asWindowName(windowHandling: ExternalNavigationWindowHandling): string | undefined {
83+
switch (windowHandling) {
84+
case ExternalNavigationWindowHandling.SameWindow:
85+
return '_self';
86+
case ExternalNavigationWindowHandling.NewWindow:
87+
return undefined;
88+
default:
89+
assertUnreachable(windowHandling);
90+
}
91+
}
92+
93+
private checkAndBuildExternalNavParams(): ExternalNavigationParams | undefined {
94+
if (isNil(this.paramsOrUrl)) {
95+
return undefined;
96+
}
97+
98+
if (typeof this.paramsOrUrl === 'string' && this.navigationService.isExternalUrl(this.paramsOrUrl)) {
99+
return {
100+
navType: NavigationParamsType.External,
101+
url: this.paramsOrUrl,
102+
windowHandling: ExternalNavigationWindowHandling.SameWindow
103+
};
104+
}
105+
106+
if (typeof this.paramsOrUrl !== 'string' && this.paramsOrUrl.navType === NavigationParamsType.External) {
107+
return {
108+
...this.paramsOrUrl,
109+
url: this.paramsOrUrl.useGlobalParams
110+
? this.navigationService.constructExternalUrl(this.paramsOrUrl.url)
111+
: this.paramsOrUrl.url
112+
};
113+
}
114+
115+
return undefined;
116+
}
46117
}
47118

48119
interface NavData {

0 commit comments

Comments
 (0)