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
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.id;
return CoreNavigator.getRouteParams(route).id;
}
}

View File

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

View File

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

View File

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

View File

@ -134,7 +134,7 @@ export class AddonModForumDiscussionPage implements OnInit, AfterViewInit, OnDes
async ngOnInit(): Promise<void> {
try {
const routeData = this.route.snapshot.data;
const routeData = CoreNavigator.getRouteData(this.route);
this.courseId = CoreNavigator.getRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRouteNumberParam('cmId');
this.forumId = CoreNavigator.getRouteNumberParam('forumId');
@ -893,9 +893,9 @@ class AddonModForumDiscussionDiscussionsSwipeManager extends AddonModForumDiscus
* @inheritdoc
*/
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> {
try {
const routeData = this.route.snapshot.data;
const routeData = CoreNavigator.getRouteData(this.route);
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId');
this.forumId = CoreNavigator.getRequiredRouteNumberParam('forumId');
@ -700,9 +700,9 @@ class AddonModForumNewDiscussionDiscussionsSwipeManager extends AddonModForumDis
* @inheritdoc
*/
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');
const entrySlug = CoreNavigator.getRequiredRouteParam<string>('entrySlug');
const routeData = this.route.snapshot.data;
const routeData = CoreNavigator.getRouteData(this.route);
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
AddonModGlossaryEntriesSource,
[this.courseId, this.cmId, routeData.glossaryPathPrefix ?? ''],
@ -368,9 +368,9 @@ class AddonModGlossaryEntryEntriesSwipeManager
* @inheritdoc
*/
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;
}
const params = route.snapshot.params;
const queryParams = route.snapshot.queryParams;
const params = CoreNavigator.getRouteParams(route);
const queryParams = CoreNavigator.getRouteQueryParams(route);
if (queryParams.subwikiId == subwikiId) {
// Same subwiki, so it's same wiki.
@ -116,7 +116,9 @@ export class AddonModWikiCreateLinkHandlerService extends CoreContentLinksHandle
if (isSameWiki) {
// 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) {
// The URL specifies which wiki it belongs to. Get the module.
const module = await CoreCourse.getModuleBasicInfoByInstance(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -307,7 +307,7 @@ export class CoreCourseProvider {
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.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':
this.swipeManager = new CoreGradesCourseCoursesSwipeManager(
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(CoreGradesCoursesSource, []),
@ -331,9 +331,7 @@ class CoreGradesCourseParticipantsSwipeManager extends CoreSwipeNavigationItemsM
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.userId;
return CoreNavigator.getRouteParams(route).userId;
}
}

View File

@ -117,7 +117,7 @@ export class CoreUserProfilePage implements OnInit, OnDestroy {
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 source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
CoreUserParticipantsSource,
@ -252,9 +252,7 @@ class CoreUserSwipeItemsManager extends CoreSwipeNavigationItemsManager {
* @inheritdoc
*/
protected getSelectedItemPathFromRoute(route: ActivatedRouteSnapshot | ActivatedRoute): string | null {
const snapshot = route instanceof ActivatedRouteSnapshot ? route : route.snapshot;
return snapshot.params.userId;
return CoreNavigator.getRouteParams(route).userId;
}
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
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';
@ -32,6 +32,7 @@ import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-deleg
import { CorePlatform } from '@services/platform';
import { filter } from 'rxjs/operators';
import { CorePromisedValue } from '@classes/promised-value';
import { BehaviorSubject } from 'rxjs';
/**
* Redirect payload.
@ -459,7 +460,7 @@ export class CoreNavigatorService {
return route;
}
if (routeData && CoreUtils.basicLeftCompare(routeData, route.snapshot.data, 3)) {
if (routeData && CoreUtils.basicLeftCompare(routeData, this.getRouteData(route), 3)) {
return route;
}
@ -477,11 +478,11 @@ export class CoreNavigatorService {
* @returns Whether the route is active or not.
*/
isRouteActive(route: ActivatedRoute): boolean {
const routePath = this.getRouteFullPath(route.snapshot);
const routePath = this.getRouteFullPath(route);
let activeRoute: ActivatedRoute | null = Router.routerState.root;
while (activeRoute) {
if (this.getRouteFullPath(activeRoute.snapshot) === routePath) {
if (this.getRouteFullPath(activeRoute) === routePath) {
return true;
}
@ -650,13 +651,13 @@ export class CoreNavigatorService {
* @param route Route snapshot.
* @returns Path.
*/
getRouteFullPath(route: ActivatedRouteSnapshot | null): string {
getRouteFullPath(route: ActivatedRouteSnapshot | ActivatedRoute | null): string {
if (!route) {
return '';
}
const parentPath = this.getRouteFullPath(route.parent);
const routePath = route.url.join('/');
const parentPath = this.getRouteFullPath(this.getRouteParent(route));
const routePath = this.getRouteUrl(route).join('/');
if (!parentPath && !routePath) {
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.
*
* @returns Whether the current route page can block leaving the route.
*/
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);
}
/**
* 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);