MOBILE-4470 angular: Fix snapshot types

Route snapshots are typed as non-optional, but we found a situation where it was undefined. Looking at the Angular source code, it seems like indeed snapshots can be undefined but they have been declared with a definite assignment assertion: https://github.com/angular/angular/blob/17.3.0/packages/router/src/router_state.ts#L231
main
Noel De Martin 2024-05-14 11:38:34 +02:00
parent daa16a32a4
commit 77d3ac9d43
19 changed files with 135 additions and 58 deletions

View File

@ -0,0 +1,13 @@
diff --git a/node_modules/@angular/router/index.d.ts b/node_modules/@angular/router/index.d.ts
index b8d7cc8..6511edf 100755
--- a/node_modules/@angular/router/index.d.ts
+++ b/node_modules/@angular/router/index.d.ts
@@ -58,7 +58,7 @@ export declare class ActivatedRoute {
/** The component of the route, a constant. */
component: Type<any> | null;
/** The current snapshot of this route */
- snapshot: ActivatedRouteSnapshot;
+ snapshot?: ActivatedRouteSnapshot;
/** An Observable of the resolved route title */
readonly title: Observable<string | undefined>;
/** An observable of the URL segments matched by this route. */

View File

@ -675,9 +675,7 @@ class AddonCalendarEventsSwipeItemsManager extends CoreSwipeNavigationItemsManag
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).id;
return snapshot.params.id;
} }
} }

View File

@ -351,9 +351,7 @@ class AddonCompetencyCompetenciesSwipeManager
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).competencyId;
return snapshot.params.competencyId;
} }
} }

View File

@ -246,9 +246,7 @@ class AddonModAssignSubmissionSwipeItemsManager extends CoreSwipeNavigationItems
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).submitId;
return snapshot.params.submitId;
} }
} }

View File

@ -188,9 +188,7 @@ class AddonModFeedbackAttemptsSwipeManager extends CoreSwipeNavigationItemsManag
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).attemptId;
return snapshot.params.attemptId;
} }
} }

View File

@ -134,7 +134,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try { try {
const routeData = this.route.snapshot.data; const routeData = CoreNavigator.getRouteData(this.route);
this.courseId = CoreNavigator.getRouteNumberParam('courseId'); this.courseId = CoreNavigator.getRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRouteNumberParam('cmId'); this.cmId = CoreNavigator.getRouteNumberParam('cmId');
this.forumId = CoreNavigator.getRouteNumberParam('forumId'); this.forumId = CoreNavigator.getRouteNumberParam('forumId');
@ -893,9 +893,9 @@ class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscus
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; const params = CoreNavigator.getRouteParams(route);
return this.getSource().DISCUSSIONS_PATH_PREFIX + snapshot.params.discussionId; return this.getSource().DISCUSSIONS_PATH_PREFIX + params.discussionId;
} }
} }

View File

@ -125,7 +125,7 @@ export class AddonModForumNewDiscussionPage implements OnInit, OnDestroy, CanLea
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
try { try {
const routeData = this.route.snapshot.data; const routeData = CoreNavigator.getRouteData(this.route);
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId'); this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
@ -700,9 +700,9 @@ class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDis
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; const params = CoreNavigator.getRouteParams(route);
return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${snapshot.params.timeCreated}`; return `${this.getSource().DISCUSSIONS_PATH_PREFIX}new/${params.timeCreated}`;
} }
} }

View File

@ -103,7 +103,7 @@ export class AddonModGlossaryEntryPage implements OnInit, OnDestroy {
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug'); const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
const routeData = this.route.snapshot.data; const routeData = CoreNavigator.getRouteData(this.route);
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource, AddonModGlossaryEntriesSource,
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''], [this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
@ -368,9 +368,9 @@ class AddonModGlossaryEntryEntriesSwipeManager
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; const params = CoreNavigator.getRouteParams(route);
return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${snapshot.params.entrySlug}`; return `${this.getSource().GLOSSARY_PATH_PREFIX}entry/${params.entrySlug}`;
} }
} }

View File

@ -49,8 +49,8 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
return false; return false;
} }
const params = route.snapshot.params; const params = CoreNavigator.getRouteParams(route);
const queryParams = route.snapshot.queryParams; const queryParams = CoreNavigator.getRouteQueryParams(route);
if (queryParams.subwikiId == subwikiId) { if (queryParams.subwikiId == subwikiId) {
// Same subwiki, so it's same wiki. // Same subwiki, so it's same wiki.
@ -116,7 +116,9 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
if (isSameWiki) { if (isSameWiki) {
// User is seeing the wiki, we can get the module from the wiki params. // User is seeing the wiki, we can get the module from the wiki params.
path = path + `/${route.snapshot.params.courseId}/${route.snapshot.params.cmId}/edit`; const routeParams = CoreNavigator.getRouteParams(route);
path = path + `/${routeParams.courseId}/${routeParams.cmId}/edit`;
} else if (wikiId) { } else if (wikiId) {
// The URL specifies which wiki it belongs to. Get the module. // The URL specifies which wiki it belongs to. Get the module.
const module = await CoreCourse.getModuleBasicInfoByInstance( const module = await CoreCourse.getModuleBasicInfoByInstance(

View File

@ -212,9 +212,7 @@ class AddonNotificationSwipeItemsManager extends CoreSwipeNavigationItemsManager
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).id;
return snapshot.params.id;
} }
} }

View File

@ -245,9 +245,7 @@ export class CoreListItemsManager<
while (route.firstChild) { while (route.firstChild) {
route = route.firstChild; route = route.firstChild;
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; segments.push(...CoreNavigator.getRouteUrl(route));
segments.push(...snapshot.url);
} }
return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null; return segments.map(segment => segment.path).join('/').replace(/\/+/, '/').trim() || null;
@ -276,7 +274,7 @@ export class CoreListItemsManager<
*/ */
private buildRouteMatcher(): (route: ActivatedRouteSnapshot) => boolean { private buildRouteMatcher(): (route: ActivatedRouteSnapshot) => boolean {
if (this.pageRouteLocator instanceof ActivatedRoute) { if (this.pageRouteLocator instanceof ActivatedRoute) {
const pageRoutePath = CoreNavigator.getRouteFullPath(this.pageRouteLocator.snapshot); const pageRoutePath = CoreNavigator.getRouteFullPath(this.pageRouteLocator);
return route => CoreNavigator.getRouteFullPath(route) === pageRoutePath; return route => CoreNavigator.getRouteFullPath(route) === pageRoutePath;
} }

View File

@ -85,9 +85,7 @@ export class CoreSwipeNavigationItemsManager<
const segments: UrlSegment[] = []; const segments: UrlSegment[] = [];
while (route) { while (route) {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; segments.push(...CoreNavigator.getRouteUrl(route));
segments.push(...snapshot.url);
if (!route.firstChild) { if (!route.firstChild) {
break; break;

View File

@ -13,10 +13,11 @@
// limitations under the License. // limitations under the License.
import { mock, mockSingleton } from '@/testing/utils'; import { mock, mockSingleton } from '@/testing/utils';
import { ActivatedRoute, ActivatedRouteSnapshot, UrlSegment } from '@angular/router'; import { ActivatedRoute, UrlSegment } from '@angular/router';
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source'; import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager'; import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { BehaviorSubject } from 'rxjs';
interface Item { interface Item {
path: string; path: string;
@ -61,9 +62,7 @@ describe('CoreSwipeNavigationItemsManager', () => {
mockSingleton(CoreNavigator, { mockSingleton(CoreNavigator, {
navigate: jest.fn(), navigate: jest.fn(),
getCurrentRoute: () => mock<ActivatedRoute>({ getCurrentRoute: () => mock<ActivatedRoute>({
snapshot: mock<ActivatedRouteSnapshot>({ url: new BehaviorSubject([mock<UrlSegment>({ path: currentPath })]),
url: [mock<UrlSegment>({ path: currentPath })],
}),
}), }),
}); });

View File

@ -95,7 +95,7 @@ export class CoreSplitViewComponent implements AfterViewInit, OnDestroy {
this.updateClasses(); this.updateClasses();
this.outletRouteSubject.next(outletRoute); this.outletRouteSubject.next(outletRoute ?? null);
} }
/** /**

View File

@ -111,7 +111,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
// Increase route depth. // Increase route depth.
const path = CoreNavigator.getRouteFullPath(this.route.snapshot); const path = CoreNavigator.getRouteFullPath(this.route);
CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, '')); CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, ''));
@ -247,7 +247,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
const path = CoreNavigator.getRouteFullPath(this.route.snapshot); const path = CoreNavigator.getRouteFullPath(this.route);
CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, '')); CoreNavigator.decreaseRouteDepth(path.replace(/(\/deep)+/, ''));
this.selectTabObserver?.off(); this.selectTabObserver?.off();

View File

@ -307,7 +307,7 @@ export class CoreCourseProvider {
return false; return false;
} }
return Number(route.snapshot.params.courseId) == courseId; return Number(CoreNavigator.getRouteParams(route).courseId) == courseId;
} }
/** /**

View File

@ -79,7 +79,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
this.collapseLabel = Translate.instant('core.collapse'); this.collapseLabel = Translate.instant('core.collapse');
this.useLegacyLayout = !CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.1'); this.useLegacyLayout = !CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('4.1');
switch (route.snapshot.data.swipeManagerSource ?? route.snapshot.parent?.data.swipeManagerSource) { switch (route.snapshot?.data.swipeManagerSource ?? route.snapshot?.parent?.data.swipeManagerSource) {
case 'courses': case 'courses':
this.swipeManager = new CoreGradesCourseCoursesSwipeManager( this.swipeManager = new CoreGradesCourseCoursesSwipeManager(
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []), CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []),
@ -331,9 +331,7 @@ class CoreGradesCourseParticipantsSwipeManager extends CoreSwipeNavigationItemsM
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).userId;
return snapshot.params.userId;
} }
} }

View File

@ -117,7 +117,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
this.courseId = undefined; this.courseId = undefined;
} }
if (this.courseId && this.route.snapshot.data.swipeManagerSource === 'participants') { if (this.courseId && CoreNavigator.getRouteData(this.route).swipeManagerSource === 'participants') {
const search = CoreNavigator.getRouteParam('search'); const search = CoreNavigator.getRouteParam('search');
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource( const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
CoreUserParticipantsSource, CoreUserParticipantsSource,
@ -252,9 +252,7 @@ class CoreUserSwipeItemsManager extends CoreSwipeNavigationItemsManager {
* @inheritdoc * @inheritdoc
*/ */
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null { protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot; return CoreNavigator.getRouteParams(route).userId;
return snapshot.params.userId;
} }
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Params } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, Data, NavigationEnd, Params, UrlSegment } from '@angular/router';
import { NavigationOptions } from '@ionic/angular/common/providers/nav-controller'; import { NavigationOptions } from '@ionic/angular/common/providers/nav-controller';
@ -32,6 +32,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg
import { CorePlatform } from '@services/platform'; import { CorePlatform } from '@services/platform';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { BehaviorSubject } from 'rxjs';
/** /**
* Redirect payload. * Redirect payload.
@ -459,7 +460,7 @@ export class CoreNavigatorService {
return route; return route;
} }
if (routeData && CoreUtils.basicLeftCompare(routeData, route.snapshot.data, 3)) { if (routeData && CoreUtils.basicLeftCompare(routeData, this.getRouteData(route), 3)) {
return route; return route;
} }
@ -477,11 +478,11 @@ export class CoreNavigatorService {
* @returns Whether the route is active or not. * @returns Whether the route is active or not.
*/ */
isRouteActive(route: ActivatedRoute): boolean { isRouteActive(route: ActivatedRoute): boolean {
const routePath = this.getRouteFullPath(route.snapshot); const routePath = this.getRouteFullPath(route);
let activeRoute: ActivatedRoute | null = Router.routerState.root; let activeRoute: ActivatedRoute | null = Router.routerState.root;
while (activeRoute) { while (activeRoute) {
if (this.getRouteFullPath(activeRoute.snapshot) === routePath) { if (this.getRouteFullPath(activeRoute) === routePath) {
return true; return true;
} }
@ -650,13 +651,13 @@ export class CoreNavigatorService {
* @param route Route snapshot. * @param route Route snapshot.
* @returns Path. * @returns Path.
*/ */
getRouteFullPath(route: ActivatedRouteSnapshot | null): string { getRouteFullPath(route: ActivatedRouteSnapshot | ActivatedRoute | null): string {
if (!route) { if (!route) {
return ''; return '';
} }
const parentPath = this.getRouteFullPath(route.parent); const parentPath = this.getRouteFullPath(this.getRouteParent(route));
const routePath = route.url.join('/'); const routePath = this.getRouteUrl(route).join('/');
if (!parentPath && !routePath) { if (!parentPath && !routePath) {
return ''; return '';
@ -669,13 +670,63 @@ export class CoreNavigatorService {
} }
} }
/**
* Given a route, get url segments.
*
* @param route Route.
* @returns Url segments.
*/
getRouteUrl(route: ActivatedRouteSnapshot | ActivatedRoute): UrlSegment[] {
return this.getRouteProperty(route, 'url', []);
}
/**
* Given a route, get its parent.
*
* @param route Route.
* @returns Parent.
*/
getRouteParent(route: ActivatedRouteSnapshot | ActivatedRoute): ActivatedRouteSnapshot | ActivatedRoute | null {
return this.getRouteProperty(route, 'parent', null);
}
/**
* Given a route, get its data.
*
* @param route Route.
* @returns Data.
*/
getRouteData(route: ActivatedRouteSnapshot | ActivatedRoute): Data {
return this.getRouteProperty(route, 'data', {});
}
/**
* Given a route, get its params.
*
* @param route Route.
* @returns Params.
*/
getRouteParams(route: ActivatedRouteSnapshot | ActivatedRoute): Params {
return this.getRouteProperty(route, 'params', {});
}
/**
* Given a route, get its query params.
*
* @param route Route.
* @returns Query params.
*/
getRouteQueryParams(route: ActivatedRouteSnapshot | ActivatedRoute): Params {
return this.getRouteProperty(route, 'queryParams', {});
}
/** /**
* Check if the current route page can block leaving the route. * Check if the current route page can block leaving the route.
* *
* @returns Whether the current route page can block leaving the route. * @returns Whether the current route page can block leaving the route.
*/ */
currentRouteCanBlockLeave(): boolean { currentRouteCanBlockLeave(): boolean {
return !!this.getCurrentRoute().snapshot.routeConfig?.canDeactivate?.length; return !!this.getCurrentRoute().snapshot?.routeConfig?.canDeactivate?.length;
} }
/** /**
@ -725,6 +776,36 @@ export class CoreNavigatorService {
return '../'.repeat(depth); return '../'.repeat(depth);
} }
/**
* Given a route, get one of its properties.
*
* @param route Route.
* @param property Route property.
* @param defaultValue Fallback value if the property is not set.
* @returns Property value.
*/
private getRouteProperty<T extends keyof ActivatedRouteSnapshot>(
route: ActivatedRouteSnapshot | ActivatedRoute,
property: T,
defaultValue: ActivatedRouteSnapshot[T],
): ActivatedRouteSnapshot[T] {
if (route instanceof ActivatedRouteSnapshot) {
return route[property];
}
if (route.snapshot instanceof ActivatedRouteSnapshot) {
return route.snapshot[property];
}
const propertyObservable = route[property];
if (propertyObservable instanceof BehaviorSubject) {
return propertyObservable.value;
}
return defaultValue;
}
} }
export const CoreNavigator = makeSingleton(CoreNavigatorService); export const CoreNavigator = makeSingleton(CoreNavigatorService);