Skip to content

Commit 6a9de9d

Browse files
authored
Merge pull request #427 from medizininformatik-initiative/416-support-loading-of-query-by-id-via-url-params
416 support loading of query by id via url params
2 parents be0c7b6 + 434182e commit 6a9de9d

File tree

8 files changed

+159
-22
lines changed

8 files changed

+159
-22
lines changed

src/app/app-paths.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const PathSegments = {
1111
search: 'search',
1212
editor: 'editor',
1313
result: 'result',
14+
loadQuery: 'load-query',
1415
cohortDefinition: 'cohort-definition',
1516
dataSelection: 'data-selection',
1617
criterion: 'criterion',

src/app/core/auth/oauth-init.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { OAuthService, AuthConfig } from 'angular-oauth2-oidc';
33
import { from, race, of, timer, Observable, throwError } from 'rxjs';
44
import { catchError, mapTo, map } from 'rxjs/operators';
55
import { IAppConfig } from 'src/app/config/app-config.model';
6-
import { environment } from 'src/environments/environment';
76

87
@Injectable({
98
providedIn: 'root',
@@ -62,6 +61,7 @@ export class OAuthInitService {
6261
const CLIENT_ID = config.auth.clientId;
6362

6463
const authConfig: AuthConfig = {
64+
preserveRequestedRoute: true,
6565
issuer: `${BASE_URL}/realms/${REALM}`,
6666
clientId: CLIENT_ID,
6767
responseType: 'code',
Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { catchError } from 'rxjs/operators';
1+
import { catchError, tap } from 'rxjs/operators';
22
import { Injectable } from '@angular/core';
3-
import { OAuthService, OAuthStorage } from 'angular-oauth2-oidc';
3+
import { OAuthService } from 'angular-oauth2-oidc';
44
import { Observable, throwError } from 'rxjs';
55
import {
66
HttpErrorResponse,
@@ -16,42 +16,50 @@ export class OAuthInterceptor implements HttpInterceptor {
1616
excludedUrls = ['assets', '/assets'];
1717
excludedUrlsRegEx = this.excludedUrls.map((url) => new RegExp('^' + url, 'i'));
1818

19-
constructor(
20-
private oauthService: OAuthService,
21-
private authStorage: OAuthStorage,
22-
private snackbar: SnackbarService
23-
) {}
19+
constructor(private oauthService: OAuthService, private snackbar: SnackbarService) {}
2420

2521
private isExcluded(req: HttpRequest<any>): boolean {
2622
return this.excludedUrlsRegEx.some((toBeExcluded) => toBeExcluded.test(req.url));
2723
}
24+
2825
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
2926
if (this.isExcluded(req)) {
3027
return next.handle(req);
3128
}
32-
const token = this.authStorage.getItem('access_token');
33-
const headers = req.headers.set('Authorization', 'Bearer ' + token);
34-
req = req.clone({ headers });
29+
const token = this.getToken();
30+
if (token) {
31+
const headers = req.headers.set('Authorization', 'Bearer ' + token);
32+
req = req.clone({ headers });
33+
}
3534
return next.handle(req).pipe(
3635
catchError((error: HttpErrorResponse) => {
36+
console.error('OAuthInterceptor: Error occurred', error);
3737
if (error.status === 401) {
3838
this.oauthService.logOut();
3939
}
4040
if (error.status === 404) {
4141
this.handleErrorCodes(error.status);
4242
}
43-
if (error.error.issue) {
43+
if (error.error?.issue) {
4444
this.handleErrorCodes(error.error.issue[0]?.code);
4545
}
46-
if (error.error.issues) {
47-
this.handleErrorCodes(error.error.issues[0]?.code, error.headers.get('Retry-After'));
46+
if (error.error?.issues) {
47+
const retryAfter = error.headers.get('Retry-After');
48+
this.handleErrorCodes(error.error.issues[0]?.code, Number(retryAfter));
4849
}
4950
return throwError(error);
5051
})
5152
);
5253
}
5354

54-
public handleErrorCodes(errorCode, retryAfter?) {
55+
private getToken(): string | null {
56+
if (this.oauthService.hasValidAccessToken()) {
57+
return this.oauthService.getAccessToken();
58+
}
59+
return null;
60+
}
61+
62+
public handleErrorCodes(errorCode, retryAfter?: number) {
5563
this.snackbar.displayErrorMessage(errorCode, retryAfter);
5664
}
5765
}

src/app/modules/data-query/data-query-routing.module.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { NgModule } from '@angular/core';
2-
import { RouterModule, Routes } from '@angular/router';
31
import { CohortDefinitionComponent } from './data-query/cohort-definition/cohort-definition.component';
42
import { DataSelectionComponent } from './data-query/data-selection/data-selection.component';
3+
import { LoadQueryIntoEditorFromUrlService } from 'src/app/service/Resolver/LoadQueryIntoEditorFromUrl.service';
4+
import { NgModule } from '@angular/core';
55
import { PathSegments } from 'src/app/app-paths';
6+
import { RouterModule, Routes } from '@angular/router';
67

78
const routes: Routes = [
89
{
@@ -11,6 +12,14 @@ const routes: Routes = [
1112
pathMatch: 'full',
1213
data: { animation: 'Search', title: 'TAB_TITLE.DATA_QUERY.COHORT_DEFINITION' },
1314
},
15+
{
16+
path: PathSegments.loadQuery,
17+
resolve: {
18+
preLoadedQuery: LoadQueryIntoEditorFromUrlService,
19+
},
20+
component: CohortDefinitionComponent,
21+
data: { animation: 'Cohort' },
22+
},
1423
{
1524
path: PathSegments.cohortDefinition,
1625
component: CohortDefinitionComponent,

src/app/service/DataQuery/Persistence/ReadDataQuery.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class ReadDataQueryService {
4646

4747
public readDataQueryById(id: number): Observable<SavedDataQuery> {
4848
return this.dataQueryApiService.getDataQueryById(id).pipe(
49-
switchMap((data) => {
49+
switchMap((data: SavedDataQueryData) => {
5050
try {
5151
TypeAssertion.assertSavedDataQueryData(data);
5252
return this.transformDataQuery(data);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { ActivatedRouteSnapshot } from '@angular/router';
2+
import { catchError, map, Observable, of } from 'rxjs';
3+
import { ConsentService } from '../Consent/Consent.service';
4+
import { DataQueryStorageService } from '../DataQuery/DataQueryStorage.service';
5+
import { Injectable } from '@angular/core';
6+
import { NavigationHelperService } from '../NavigationHelper.service';
7+
import { SavedDataQuery } from 'src/app/model/SavedDataQuery/SavedDataQuery';
8+
import { SnackbarService } from 'src/app/shared/service/Snackbar/Snackbar.service';
9+
import { UiCRTDL } from 'src/app/model/UiCRTDL';
10+
11+
/**
12+
* This resolver extracts query ID from route parameters, validates it, and loads the corresponding
13+
* saved query data to initialize the editor state.
14+
*
15+
* Used as a route resolver to preload query data before component initialization.
16+
*/
17+
@Injectable({
18+
providedIn: 'root',
19+
})
20+
export class LoadQueryIntoEditorFromUrlService {
21+
constructor(
22+
private dataQueryStorageService: DataQueryStorageService,
23+
private consentService: ConsentService,
24+
private snackbarService: SnackbarService,
25+
private navigationHelper: NavigationHelperService
26+
) {}
27+
28+
/**
29+
* Main resolver method that orchestrates the query loading process.
30+
*
31+
* @param route - The activated route snapshot containing route parameters and query params
32+
* @returns Observable of the loaded saved query or undefined if loading fails
33+
*/
34+
resolve(route: ActivatedRouteSnapshot): Observable<UiCRTDL> | undefined {
35+
this.clearPreviousConsent();
36+
const queryId = this.extractAndValidateQueryId(route);
37+
if (queryId === null) {
38+
return undefined;
39+
}
40+
return this.loadSavedQuery(queryId);
41+
}
42+
43+
/**
44+
* This prevents consent from previous queries affecting the current query loading.
45+
* @returns void
46+
*/
47+
private clearPreviousConsent(): void {
48+
this.consentService.clearConsent();
49+
}
50+
51+
/**
52+
* Extracts the query ID from route parameters and validates its format and value.
53+
*
54+
* @param route - The activated route snapshot containing query parameters
55+
* @returns The validated query ID as a number, or null if validation fails
56+
*/
57+
private extractAndValidateQueryId(route: ActivatedRouteSnapshot): number | null {
58+
const idParam = route.queryParams.id;
59+
if (!idParam) {
60+
console.warn('No "id" parameter in query string.');
61+
return null;
62+
}
63+
return this.parseAndValidateId(idParam);
64+
}
65+
66+
/**
67+
* Ensures the ID is a positive, safe integer.
68+
* @param idParam - The raw ID parameter value to parse and validate
69+
* @returns The parsed and validated ID, or null if validation fails
70+
*/
71+
private parseAndValidateId(idParam: string): number | null {
72+
const id = Number(idParam);
73+
74+
if (isNaN(id) || id <= 0 || !Number.isSafeInteger(id)) {
75+
console.warn(`Invalid "id" parameter: ${idParam}. Must be a positive integer.`);
76+
return null;
77+
}
78+
return id;
79+
}
80+
81+
/**
82+
* Loads the saved query from storage using the validated query ID.
83+
*
84+
* @param queryId - The validated query ID to load
85+
* @returns Observable that emits the loaded saved query with processed CRTDL data
86+
*/
87+
private loadSavedQuery(queryId: number): Observable<UiCRTDL> {
88+
return this.dataQueryStorageService.readDataQueryById(queryId).pipe(
89+
map((savedQuery: SavedDataQuery) => savedQuery.getCrtdl()),
90+
catchError((error) => {
91+
this.snackbarService.displayErrorMessageWithNoCode('Error loading saved query, not found');
92+
this.navigationHelper.navigateToDataQueryCohortDefinition();
93+
return of(null);
94+
})
95+
);
96+
}
97+
}

src/app/shared/components/snack-bar/snackbar.component.scss

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
width: 100%;
33
height: 50px;
44
display: flex;
5-
justify-content: center; /* Center text horizontally */
6-
align-items: center; /* Center text vertically */
5+
justify-content: center;
6+
/* Center text horizontally */
7+
align-items: center;
8+
/* Center text vertically */
79
padding: 0 10px;
810
color: white;
911
}
@@ -13,16 +15,23 @@
1315
}
1416

1517
.info-snackbar {
16-
background-color: var(--num-color--primary-green); /* Info color */
18+
background-color: var(--num-color--primary-green);
19+
/* Info color */
1720
}
1821

1922
.close-icon {
20-
margin-left: auto; /* Pushes the close button to the right */
23+
margin-left: auto;
24+
/* Pushes the close button to the right */
2125
color: black;
2226
}
2327

2428
.message-display {
2529
padding-left: 13%;
2630
font-size: larger;
2731
color: rgba(171, 42, 42, 1);
32+
font-weight: 500;
33+
font-style: Medium;
34+
font-size: large;
35+
line-height: 100%;
36+
letter-spacing: 0%;
2837
}

src/app/shared/service/Snackbar/Snackbar.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@ export class SnackbarService {
3636

3737
constructor() {}
3838

39+
/**
40+
* @todo implement timer for retry after
41+
* @param errorCode
42+
* @param retryAfter
43+
*/
3944
public displayErrorMessage(errorCode: string, retryAfter: number = 0) {
4045
const message = `${MessageType.ERROR}.${errorCode}`;
46+
// if (retryAfter > 0) {
47+
// message += `${retryAfter}`;
48+
// this.setSnackbarTimeOut(retryAfter * 1000);
49+
// }
4150
this.activateSnackbar(message, SnackbarColor.ERROR);
4251
}
4352

@@ -56,6 +65,10 @@ export class SnackbarService {
5665
setTimeout(() => this.deactivateSnackbar(), 5000);
5766
}
5867

68+
private setSnackbarTimeOut(timeout: number = 5000) {
69+
setTimeout(() => this.deactivateSnackbar(), timeout);
70+
}
71+
5972
public deactivateSnackbar() {
6073
this.visibilitySubject.next(false);
6174
}

0 commit comments

Comments
 (0)