diff --git a/src/addons/badges/pages/user-badges/user-badges.page.ts b/src/addons/badges/pages/user-badges/user-badges.page.ts index d1ea67af6..23b54a499 100644 --- a/src/addons/badges/pages/user-badges/user-badges.page.ts +++ b/src/addons/badges/pages/user-badges/user-badges.page.ts @@ -19,7 +19,7 @@ import { CoreTimeUtils } from '@services/utils/time'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { ActivatedRoute } from '@angular/router'; // @todo import { CoreSplitViewComponent } from '@components/split-view/split-view'; @@ -107,7 +107,7 @@ export class AddonBadgesUserBadgesPage implements OnInit { const params = { courseId: this.courseId, userId: this.userId, badgeHash: badgeHash }; // @todo use splitview. // this.splitviewCtrl.push('AddonBadgesIssuedBadgePage', params); - CoreNavHelper.instance.goInSite('/badges/issue', params); + CoreNavigator.instance.navigateToSitePath('/badges/issue', { params }); } } diff --git a/src/addons/badges/services/handlers/badge-link.ts b/src/addons/badges/services/handlers/badge-link.ts index 13d54b376..7b09881ef 100644 --- a/src/addons/badges/services/handlers/badge-link.ts +++ b/src/addons/badges/services/handlers/badge-link.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { AddonBadges } from '../badges'; @@ -43,10 +43,12 @@ export class AddonBadgesBadgeLinkHandlerService extends CoreContentLinksHandlerB return [{ action: (siteId: string): void => { - CoreNavHelper.instance.goInSite( + CoreNavigator.instance.navigateToSitePath( '/badges/issue', - { courseId: 0, badgeHash: params.hash }, - siteId, + { + siteId, + params: { courseId: 0, badgeHash: params.hash }, + }, ); }, }]; diff --git a/src/addons/badges/services/handlers/mybadges-link.ts b/src/addons/badges/services/handlers/mybadges-link.ts index 9e0c873c9..ee7b19db5 100644 --- a/src/addons/badges/services/handlers/mybadges-link.ts +++ b/src/addons/badges/services/handlers/mybadges-link.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { AddonBadges } from '../badges'; @@ -37,7 +37,7 @@ export class AddonBadgesMyBadgesLinkHandlerService extends CoreContentLinksHandl getActions(): CoreContentLinksAction[] { return [{ action: (siteId: string): void => { - CoreNavHelper.instance.goInSite('/badges/user', {}, siteId); + CoreNavigator.instance.navigateToSitePath('/badges/user', { siteId }); }, }]; } diff --git a/src/addons/badges/services/handlers/push-click.ts b/src/addons/badges/services/handlers/push-click.ts index e31f48531..e3a834f3c 100644 --- a/src/addons/badges/services/handlers/push-click.ts +++ b/src/addons/badges/services/handlers/push-click.ts @@ -19,7 +19,7 @@ import { CorePushNotificationsClickHandler } from '@features/pushnotifications/s import { AddonBadges } from '../badges'; import { makeSingleton } from '@singletons'; import { CorePushNotificationsNotificationBasicData } from '@features/pushnotifications/services/pushnotifications'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Handler for badges push notifications clicks. @@ -59,7 +59,12 @@ export class AddonBadgesPushClickHandlerService implements CorePushNotifications if (data.hash) { // We have the hash, open the badge directly. - return CoreNavHelper.instance.goInSite('/badges/issue', { courseId: 0, badgeHash: data.hash }, notification.site); + await CoreNavigator.instance.navigateToSitePath('/badges/issue', { + siteId: notification.site, + params: { courseId: 0, badgeHash: data.hash }, + }); + + return; } // No hash, open the list of user badges. @@ -71,7 +76,7 @@ export class AddonBadgesPushClickHandlerService implements CorePushNotifications ), ); - await CoreNavHelper.instance.goInSite('/badges/user', {}, notification.site); + await CoreNavigator.instance.navigateToSitePath('/badges/user', { siteId: notification.site }); } } diff --git a/src/addons/badges/services/handlers/user.ts b/src/addons/badges/services/handlers/user.ts index aad3c5ac2..0aed8ef9c 100644 --- a/src/addons/badges/services/handlers/user.ts +++ b/src/addons/badges/services/handlers/user.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; import { CoreUserProfile } from '@features/user/services/user'; import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { AddonBadges } from '../badges'; @@ -72,7 +72,10 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler { action: (event, user, courseId): void => { event.preventDefault(); event.stopPropagation(); - CoreNavHelper.instance.goInSite('/badges/user', { courseId: courseId || 0, userId: user.id }); + CoreNavigator.instance.navigateToSitePath( + '/badges/user', + { params: { courseId: courseId || 0, userId: user.id } }, + ); }, }; } diff --git a/src/core/components/user-avatar/user-avatar.ts b/src/core/components/user-avatar/user-avatar.ts index dbbd25b31..2e8348a76 100644 --- a/src/core/components/user-avatar/user-avatar.ts +++ b/src/core/components/user-avatar/user-avatar.ts @@ -145,7 +145,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy { // @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav. this.navCtrl.navigateForward(['user'], { relativeTo: this.route, - queryParams: CoreObject.removeUndefined({ + queryParams: CoreObject.withoutEmpty({ userId: this.userId, courseId: this.courseId, }), diff --git a/src/core/directives/user-link.ts b/src/core/directives/user-link.ts index 3099ae77e..b8b2cf2c1 100644 --- a/src/core/directives/user-link.ts +++ b/src/core/directives/user-link.ts @@ -15,7 +15,7 @@ import { Directive, Input, OnInit, ElementRef } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { NavController } from '@ionic/angular'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { CoreObject } from '@singletons/object'; @@ -54,10 +54,12 @@ export class CoreUserLinkDirective implements OnInit { event.stopPropagation(); // @todo If this directive is inside a split view, use the split view's master nav. - CoreNavHelper.instance.goInCurrentMainMenuTab('user', CoreObject.removeUndefined({ - userId: this.userId, - courseId: this.courseId, - })); + CoreNavigator.instance.navigateToSitePath('user', { + params: CoreObject.withoutEmpty({ + userId: this.userId, + courseId: this.courseId, + }), + }); }); } diff --git a/src/core/features/block/components/only-title-block/only-title-block.ts b/src/core/features/block/components/only-title-block/only-title-block.ts index 3e2ec6d20..35f7b7d73 100644 --- a/src/core/features/block/components/only-title-block/only-title-block.ts +++ b/src/core/features/block/components/only-title-block/only-title-block.ts @@ -14,7 +14,7 @@ import { OnInit, Component } from '@angular/core'; import { CoreBlockBaseComponent } from '../../classes/base-block-component'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Component to render blocks with only a title and link. @@ -24,7 +24,7 @@ import { CoreNavHelper } from '@services/nav-helper'; templateUrl: 'core-block-only-title.html', styleUrls: ['only-title-block.scss'], }) -export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit { +export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent implements OnInit { constructor() { super('CoreBlockOnlyTitleComponent'); @@ -43,7 +43,8 @@ export class CoreBlockOnlyTitleComponent extends CoreBlockBaseComponent impleme * Go to the block page. */ gotoBlock(): void { - CoreNavHelper.instance.goInSite(this.link!, this.linkParams!, undefined, true); + // @todo test that this is working properly. + CoreNavigator.instance.navigateToSitePath(this.link!, { params: this.linkParams }); } } diff --git a/src/core/features/contentlinks/classes/module-list-handler.ts b/src/core/features/contentlinks/classes/module-list-handler.ts index e0359ae50..f1c5c376a 100644 --- a/src/core/features/contentlinks/classes/module-list-handler.ts +++ b/src/core/features/contentlinks/classes/module-list-handler.ts @@ -16,7 +16,6 @@ import { CoreContentLinksHandlerBase } from './base-handler'; import { Translate } from '@singletons'; import { Params } from '@angular/router'; import { CoreContentLinksAction } from '../services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; /** * Handler to handle URLs pointing to a list of a certain type of modules. @@ -56,14 +55,16 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise { return [{ + // eslint-disable-next-line @typescript-eslint/no-unused-vars action: (siteId): void => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const stateParams = { courseId: params.id, modName: this.modName, title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'), }; - CoreNavHelper.instance.goInSite('CoreCourseListModTypePage @todo', stateParams, siteId); + // @todo CoreNavigator.instance.goInSite('CoreCourseListModTypePage', stateParams, siteId); }, }]; } diff --git a/src/core/features/contentlinks/pages/choose-site/choose-site.ts b/src/core/features/contentlinks/pages/choose-site/choose-site.ts index 376de7f04..2a95d6e56 100644 --- a/src/core/features/contentlinks/pages/choose-site/choose-site.ts +++ b/src/core/features/contentlinks/pages/choose-site/choose-site.ts @@ -21,7 +21,7 @@ import { CoreContentLinksAction } from '../../services/contentlinks-delegate'; import { CoreContentLinksHelper } from '../../services/contentlinks-helper'; import { ActivatedRoute } from '@angular/router'; import { CoreError } from '@classes/errors/error'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page to display the list of sites to choose one to perform a content link action. @@ -102,7 +102,7 @@ export class CoreContentLinksChooseSitePage implements OnInit { */ siteClicked(siteId: string): void { if (this.isRootURL) { - CoreNavHelper.instance.openInSiteMainMenu('', {}, siteId); + CoreNavigator.instance.navigateToSiteHome({ siteId }); } else if (this.action) { this.action.action(siteId); } diff --git a/src/core/features/contentlinks/services/contentlinks-helper.ts b/src/core/features/contentlinks/services/contentlinks-helper.ts index 75631efce..f789767d7 100644 --- a/src/core/features/contentlinks/services/contentlinks-helper.ts +++ b/src/core/features/contentlinks/services/contentlinks-helper.ts @@ -19,8 +19,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreContentLinksDelegate, CoreContentLinksAction } from './contentlinks-delegate'; import { CoreSite } from '@classes/site'; import { makeSingleton, Translate } from '@singletons'; -import { Params } from '@angular/router'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Service that provides some features regarding content links. @@ -92,10 +91,11 @@ export class CoreContentLinksHelperProvider { * @param siteId Site ID. If not defined, current site. * @param checkMenu If true, check if the root page of a main menu tab. Only the page name will be checked. * @return Promise resolved when done. - * @deprecated since 3.9.5. Use CoreNavHelperService.goInSite instead. + * @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead. */ - goInSite(pageName: string, pageParams: Params, siteId?: string, checkMenu?: boolean): Promise { - return CoreNavHelper.instance.goInSite(pageName, pageParams, siteId, checkMenu); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string): Promise { + await CoreNavigator.instance.navigateToSitePath(pageName, { params: pageParams, siteId }); } /** @@ -192,7 +192,7 @@ export class CoreContentLinksHelperProvider { } } else { // Login in the site. - return CoreNavHelper.instance.openInSiteMainMenu('', {}, site.getId()); + await CoreNavigator.instance.navigateToSiteHome({ siteId: site.getId() }); } } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 0df7ad379..01dbf096a 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -34,7 +34,6 @@ import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses import { CoreArray } from '@singletons/array'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreCourseOffline } from './course-offline'; -import { CoreNavHelper, CoreNavHelperService } from '@services/nav-helper'; import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, @@ -979,7 +978,7 @@ export class CoreCourseHelperProvider { * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. */ - openCourse(course: CoreEnrolledCourseBasicData | { id: number }, params?: Params, siteId?: string): Promise { + async openCourse(course: CoreEnrolledCourseBasicData | { id: number }, params?: Params, siteId?: string): Promise { if (!siteId || siteId == CoreSites.instance.getCurrentSiteId()) { // Current site, we can open the course. return CoreCourse.instance.openCourse(course, params); @@ -988,7 +987,9 @@ export class CoreCourseHelperProvider { params = params || {}; Object.assign(params, { course: course }); - return CoreNavHelper.instance.openInSiteMainMenu(CoreNavHelperService.OPEN_COURSE, params, siteId); + // @todo implement open course. + // await CoreNavigator.instance.navigateToSitePath('/course/.../...', { siteId, queryParams: params }); + // return CoreNavigator.instance.openInSiteMainMenu(CoreNavigatorService.OPEN_COURSE, params, siteId); } } diff --git a/src/core/features/courses/pages/dashboard/dashboard.ts b/src/core/features/courses/pages/dashboard/dashboard.ts index cc93164a6..f32c3b666 100644 --- a/src/core/features/courses/pages/dashboard/dashboard.ts +++ b/src/core/features/courses/pages/dashboard/dashboard.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; -import { IonRefresher, NavController } from '@ionic/angular'; +import { IonRefresher } from '@ionic/angular'; import { CoreCourses, CoreCoursesProvider } from '../../services/courses'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -22,6 +22,7 @@ import { CoreCoursesDashboard } from '@features/courses/services/dashboard'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourseBlock } from '@features/course/services/course'; import { CoreBlockComponent } from '@features/block/components/block/block'; +import { CoreNavigator } from '@services/navigator'; /** * Page that displays the dashboard page. @@ -35,7 +36,6 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { @ViewChildren(CoreBlockComponent) blocksComponents?: QueryList; - searchEnabled = false; downloadEnabled = false; downloadCourseEnabled = false; @@ -47,10 +47,6 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { protected updateSiteObserver?: CoreEventObserver; - constructor( - protected navCtrl: NavController, - ) { } - /** * Initialize the component. */ @@ -171,8 +167,8 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy { /** * Go to search courses. */ - openSearch(): void { - this.navCtrl.navigateForward(['/main/home/courses/search']); + async openSearch(): Promise { + CoreNavigator.instance.navigateToSitePath('/courses/search'); } /** diff --git a/src/core/features/login/pages/change-password/change-password.ts b/src/core/features/login/pages/change-password/change-password.ts index 9872eb67a..6cb928503 100644 --- a/src/core/features/login/pages/change-password/change-password.ts +++ b/src/core/features/login/pages/change-password/change-password.ts @@ -18,7 +18,7 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { Translate } from '@singletons'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page that shows instructions to change the password. @@ -63,7 +63,7 @@ export class CoreLoginChangePasswordPage { * Login the user. */ login(): void { - CoreNavHelper.instance.goToSiteInitialPage(); + CoreNavigator.instance.navigateToSiteHome(); this.changingPassword = false; } diff --git a/src/core/features/login/pages/credentials/credentials.ts b/src/core/features/login/pages/credentials/credentials.ts index 118360ee3..34cb74c81 100644 --- a/src/core/features/login/pages/credentials/credentials.ts +++ b/src/core/features/login/pages/credentials/credentials.ts @@ -26,7 +26,7 @@ import { CoreConstants } from '@/core/constants'; import { Translate } from '@singletons'; import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site'; import { CoreEvents } from '@singletons/events'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page to enter the user credentials. @@ -245,7 +245,8 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy { this.siteId = id; - await CoreNavHelper.instance.goToSiteInitialPage({ urlToOpen: this.urlToOpen }); + // @todo test that this is working properly. + await CoreNavigator.instance.navigateToSiteHome({ params: { urlToOpen: this.urlToOpen } }); } catch (error) { CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password); diff --git a/src/core/features/login/pages/init/init.ts b/src/core/features/login/pages/init/init.ts index 97f89a85a..096dda7a0 100644 --- a/src/core/features/login/pages/init/init.ts +++ b/src/core/features/login/pages/init/init.ts @@ -20,7 +20,7 @@ import { ApplicationInit, SplashScreen } from '@singletons'; import { CoreConstants } from '@/core/constants'; import { CoreSites } from '@services/sites'; import { CoreLoginHelper } from '@features/login/services/login-helper'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -32,6 +32,8 @@ import { CoreNavHelper } from '@services/nav-helper'; }) export class CoreLoginInitPage implements OnInit { + // @todo this page should be removed in favor of native splash + // or a splash component rendered in the root app component constructor(protected navCtrl: NavController) {} /** @@ -80,17 +82,21 @@ export class CoreLoginInitPage implements OnInit { return; } - return CoreNavHelper.instance.goToSiteInitialPage({ - redirectPage: redirectData.page, - redirectParams: redirectData.params, + await CoreNavigator.instance.navigateToSiteHome({ + params: { + redirectPath: redirectData.page, + redirectParams: redirectData.params, + }, }); + + return; } catch (error) { // Site doesn't exist. return this.loadPage(); } } else if (redirectData.page) { // No site to load, open the page. - return CoreNavHelper.instance.goToNoSitePage(redirectData.page, redirectData.params); + // @todo return CoreNavigator.instance.goToNoSitePage(redirectData.page, redirectData.params); } } @@ -110,7 +116,9 @@ export class CoreLoginInitPage implements OnInit { return this.loadPage(); } - return CoreNavHelper.instance.goToSiteInitialPage(); + await CoreNavigator.instance.navigateToSiteHome(); + + return; } await this.navCtrl.navigateRoot('/login/sites'); diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index 729bcd50b..24204a371 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -25,7 +25,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site'; import { CoreEvents } from '@singletons/events'; import { CoreError } from '@classes/errors/error'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page to enter the user password to reconnect to a site. @@ -207,9 +207,12 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { this.credForm.controls['password'].reset(); // Go to the site initial page. - await CoreNavHelper.instance.goToSiteInitialPage({ - redirectPage: this.page, - redirectParams: this.pageParams, + // @todo test that this is working properly (could we use navigateToSitePath instead?). + await CoreNavigator.instance.navigateToSiteHome({ + params: { + redirectPath: this.page, + redirectParams: this.pageParams, + }, }); } catch (error) { CoreLoginHelper.instance.treatUserTokenError(this.siteUrl, error, this.username, password); diff --git a/src/core/features/login/pages/site-policy/site-policy.ts b/src/core/features/login/pages/site-policy/site-policy.ts index b38591b98..15af5cce8 100644 --- a/src/core/features/login/pages/site-policy/site-policy.ts +++ b/src/core/features/login/pages/site-policy/site-policy.ts @@ -22,7 +22,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSite } from '@classes/site'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page to accept a site policy. @@ -129,7 +129,7 @@ export class CoreLoginSitePolicyPage implements OnInit { // Invalidate cache since some WS don't return error if site policy is not accepted. await CoreUtils.instance.ignoreErrors(this.currentSite!.invalidateWsCache()); - await CoreNavHelper.instance.goToSiteInitialPage(); + await CoreNavigator.instance.navigateToSiteHome(); } catch (error) { CoreDomUtils.instance.showErrorModalDefault(error, 'Error accepting site policy.'); } finally { diff --git a/src/core/features/login/pages/site/site.ts b/src/core/features/login/pages/site/site.ts index 7d60e9871..326b6f648 100644 --- a/src/core/features/login/pages/site/site.ts +++ b/src/core/features/login/pages/site/site.ts @@ -31,7 +31,7 @@ import { CoreUrl } from '@singletons/url'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreLoginSiteHelpComponent } from '@features/login/components/site-help/site-help'; import { CoreLoginSiteOnboardingComponent } from '@features/login/components/site-onboarding/site-onboarding'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -329,7 +329,9 @@ export class CoreLoginSitePage implements OnInit { CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); - return CoreNavHelper.instance.goToSiteInitialPage(); + await CoreNavigator.instance.navigateToSiteHome(); + + return; } catch (error) { CoreLoginHelper.instance.treatUserTokenError(siteData.url, error, siteData.username, siteData.password); diff --git a/src/core/features/login/pages/sites/sites.ts b/src/core/features/login/pages/sites/sites.ts index 276bb6a94..429682697 100644 --- a/src/core/features/login/pages/sites/sites.ts +++ b/src/core/features/login/pages/sites/sites.ts @@ -19,7 +19,7 @@ import { Component, OnInit } from '@angular/core'; import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; import { CoreLogger } from '@singletons/logger'; import { CoreLoginHelper } from '@features/login/services/login-helper'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Page that displays a "splash screen" while the app is being initialized. @@ -125,7 +125,9 @@ export class CoreLoginSitesPage implements OnInit { const loggedIn = await CoreSites.instance.loadSite(siteId); if (loggedIn) { - return CoreNavHelper.instance.goToSiteInitialPage(); + await CoreNavigator.instance.navigateToSiteHome(); + + return; } } catch (error) { this.logger.error('Error loading site ' + siteId, error); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 1b50a1c7e..dade42d2e 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -34,7 +34,7 @@ import { makeSingleton, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreObject } from '@singletons/object'; -import { CoreNavHelper, CoreNavHelperOpenMainMenuOptions, CoreNavHelperService } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Helper provider that provides some common features regarding authentication. @@ -42,7 +42,11 @@ import { CoreNavHelper, CoreNavHelperOpenMainMenuOptions, CoreNavHelperService } @Injectable({ providedIn: 'root' }) export class CoreLoginHelperProvider { - static readonly OPEN_COURSE = CoreNavHelperService.OPEN_COURSE; // @deprecated since 3.9.5. + /** + * @deprecated since 3.9.5. + */ + static readonly OPEN_COURSE = 'open_course'; + static readonly ONBOARDING_DONE = 'onboarding_done'; static readonly FAQ_URL_IMAGE_HTML = ''; static readonly FAQ_QRCODE_IMAGE_HTML = ''; @@ -111,9 +115,12 @@ export class CoreLoginHelperProvider { */ checkLogout(): void { const currentSite = CoreSites.instance.getCurrentSite(); - const currentPage = CoreNavHelper.instance.getCurrentPage(); - if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') { + if ( + !CoreApp.instance.isSSOAuthenticationOngoing() && + currentSite?.isLoggedOut() && + CoreNavigator.instance.isCurrent('/login/reconnect') + ) { // User must reauthenticate but he closed the InAppBrowser without doing so, logout him. CoreSites.instance.logout(); } @@ -436,21 +443,33 @@ export class CoreLoginHelperProvider { * @param page Page to open. * @param params Params of the page. * @return Promise resolved when done. - * @deprecated since 3.9.5. Use CoreNavHelperService.goToNoSitePage instead. + * @deprecated since 3.9.5. Use CoreNavigator.navigateToLoginCredentials instead. */ - goToNoSitePage(page: string, params?: Params): Promise { - return CoreNavHelper.instance.goToNoSitePage(page, params); + async goToNoSitePage(page: string, params?: Params): Promise { + await CoreNavigator.instance.navigateToLoginCredentials(params); } /** * Go to the initial page of a site depending on 'userhomepage' setting. * - * @param options Options. + * @param navCtrl NavController to use. Defaults to app root NavController. + * @param page Name of the page to load after loading the main page. + * @param params Params to pass to the page. + * @param options Navigation options. + * @param url URL to open once the main menu is loaded. * @return Promise resolved when done. - * @deprecated since 3.9.5. Use CoreNavHelperService.goToSiteInitialPage instead. + * @deprecated since 3.9.5. Use CoreNavigator.navigateToSiteHome or CoreNavigator.navigateToSitePath instead. */ - goToSiteInitialPage(options?: CoreNavHelperOpenMainMenuOptions): Promise { - return CoreNavHelper.instance.goToSiteInitialPage(options); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async goToSiteInitialPage(navCtrl?: NavController, page?: string, params?: any, options?: any, url?: string): Promise { + await CoreNavigator.instance.navigateToSiteHome({ + ...options, + params: { + redirectPath: page, + redirectParams: params, + urlToOpen: url, + }, + }); } /** @@ -606,10 +625,10 @@ export class CoreLoginHelperProvider { * * @param page Name of the page to load. * @param params Params to pass to the page. - * @deprecated since 3.9.5. Use CoreNavHelperService.loadPageInMainMenu instead. + * @deprecated since 3.9.5. Use CoreNavigator.navigateToSitepath instead. */ loadPageInMainMenu(page: string, params?: Params): void { - CoreNavHelper.instance.loadPageInMainMenu(page, params); + CoreNavigator.instance.navigateToSitePath(page, { params }); } /** @@ -767,10 +786,8 @@ export class CoreLoginHelperProvider { return; // Site that triggered the event is not current site. } - const currentPage = CoreNavHelper.instance.getCurrentPage(); - // If current page is already change password, stop. - if (currentPage == '/login/changepassword') { + if (CoreNavigator.instance.isCurrent('/login/changepassword')) { return; } @@ -826,14 +843,14 @@ export class CoreLoginHelperProvider { /** * Redirect to a new page, setting it as the root page and loading the right site if needed. * - * @param page Name of the page to load. Special cases: CoreNavHelperService.OPEN_COURSE (to open course page). + * @param page Name of the page to load. * @param params Params to pass to the page. * @param siteId Site to load. If not defined, current site. * @return Promise resolved when done. - * @deprecated since 3.9.5. Use CoreNavHelperService.openInSiteMainMenu instead. + * @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead. */ - redirect(page: string, params?: Params, siteId?: string): Promise { - return CoreNavHelper.instance.openInSiteMainMenu(page, params, siteId); + async redirect(page: string, params?: Params, siteId?: string): Promise { + await CoreNavigator.instance.navigateToSitePath(page, { params, siteId }); } /** @@ -959,14 +976,14 @@ export class CoreLoginHelperProvider { const info = currentSite.getInfo(); if (typeof info != 'undefined' && typeof info.username != 'undefined' && !this.isOpeningReconnect) { // If current page is already reconnect, stop. - if (CoreNavHelper.instance.getCurrentPage() == '/login/reconnect') { + if (CoreNavigator.instance.isCurrent('/login/reconnect')) { return; } this.isOpeningReconnect = true; await CoreUtils.instance.ignoreErrors(this.navCtrl.navigateRoot('/login/reconnect', { - queryParams: CoreObject.removeUndefined({ + queryParams: CoreObject.withoutEmpty({ siteId, pageName: data.pageName, pageParams: data.params, @@ -1127,7 +1144,7 @@ export class CoreLoginHelperProvider { } // If current page is already site policy, stop. - if (CoreNavHelper.instance.getCurrentPage() == '/login/sitepolicy') { + if (CoreNavigator.instance.isCurrent('/login/sitepolicy')) { return; } diff --git a/src/core/features/mainmenu/pages/menu/menu.html b/src/core/features/mainmenu/pages/menu/menu.html index 148c37584..c86ab2ea3 100644 --- a/src/core/features/mainmenu/pages/menu/menu.html +++ b/src/core/features/mainmenu/pages/menu/menu.html @@ -2,9 +2,6 @@ - - - diff --git a/src/core/features/mainmenu/pages/menu/menu.ts b/src/core/features/mainmenu/pages/menu/menu.ts index 9e99e239e..45dbddbe4 100644 --- a/src/core/features/mainmenu/pages/menu/menu.ts +++ b/src/core/features/mainmenu/pages/menu/menu.ts @@ -13,19 +13,19 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef } from '@angular/core'; -import { ActivatedRoute, Params, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { NavController, IonTabs } from '@ionic/angular'; import { Subscription } from 'rxjs'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreEvents, CoreEventObserver, CoreEventLoadPageMainMenuData } from '@singletons/events'; +import { CoreEvents, CoreEventObserver } from '@singletons/events'; import { CoreMainMenu } from '../../services/mainmenu'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate'; import { CoreDomUtils } from '@services/utils/dom'; import { Translate } from '@singletons'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreRedirectPayload } from '@services/navigator'; /** * Page that displays the main menu of the app. @@ -40,17 +40,14 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { tabs: CoreMainMenuHandlerToDisplay[] = []; allHandlers?: CoreMainMenuHandlerToDisplay[]; loaded = false; - redirectPage?: string; - redirectParams?: Params; showTabs = false; tabsPlacement = 'bottom'; hidden = false; protected subscription?: Subscription; protected redirectObs?: CoreEventObserver; - protected pendingRedirect?: CoreEventLoadPageMainMenuData; + protected pendingRedirect?: CoreRedirectPayload; protected urlToOpen?: string; - protected mainMenuId: number; protected keyboardObserver?: CoreEventObserver; @ViewChild('mainTabs') mainTabs?: IonTabs; @@ -60,44 +57,32 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { protected navCtrl: NavController, protected changeDetector: ChangeDetectorRef, protected router: Router, - ) { - this.mainMenuId = CoreNavHelper.instance.getMainMenuId(); - } + ) {} /** * Initialize the component. */ ngOnInit(): void { + // @TODO this should be handled by route guards and can be removed if (!CoreSites.instance.isLoggedIn()) { this.navCtrl.navigateRoot('/login/init'); return; } - this.route.queryParams.subscribe(params => { - const redirectPage = params['redirectPage']; - if (redirectPage) { + this.route.queryParams.subscribe((params: Partial & { urlToOpen?: string }) => { + if (params.redirectPath) { this.pendingRedirect = { - redirectPage: redirectPage, - redirectParams: params['redirectParams'], + redirectPath: params.redirectPath, + redirectParams: params.redirectParams, }; } - this.urlToOpen = params['urlToOpen']; + this.urlToOpen = params.urlToOpen; }); this.showTabs = true; - this.redirectObs = CoreEvents.on(CoreEvents.LOAD_PAGE_MAIN_MENU, (data: CoreEventLoadPageMainMenuData) => { - if (!this.loaded) { - // View isn't ready yet, wait for it to be ready. - this.pendingRedirect = data; - } else { - delete this.pendingRedirect; - this.handleRedirect(data); - } - }); - this.subscription = CoreMainMenuDelegate.instance.getHandlersObservable().subscribe((handlers) => { // Remove the handlers that should only appear in the More menu. this.allHandlers = handlers.filter((handler) => !handler.onlyInMore); @@ -131,8 +116,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { } }); } - - CoreNavHelper.instance.setMainMenuOpen(this.mainMenuId, true); } /** @@ -201,13 +184,13 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { * * @param data Data received. */ - protected handleRedirect(data: CoreEventLoadPageMainMenuData): void { + protected handleRedirect(data: CoreRedirectPayload): void { // Check if the redirect page is the root page of any of the tabs. - const i = this.tabs.findIndex((tab) => tab.page == data.redirectPage); + const i = this.tabs.findIndex((tab) => tab.page == data.redirectPath); if (i >= 0) { // Tab found. Open it with the params. - this.navCtrl.navigateForward(data.redirectPage, { + this.navCtrl.navigateForward(data.redirectPath, { queryParams: data.redirectParams, animated: false, }); @@ -227,7 +210,6 @@ export class CoreMainMenuPage implements OnInit, OnDestroy { this.subscription?.unsubscribe(); this.redirectObs?.off(); window.removeEventListener('resize', this.initHandlers.bind(this)); - CoreNavHelper.instance.setMainMenuOpen(this.mainMenuId, false); this.keyboardObserver?.off(); } diff --git a/src/core/features/mainmenu/services/mainmenu.ts b/src/core/features/mainmenu/services/mainmenu.ts index 42a9a51b0..ac344f718 100644 --- a/src/core/features/mainmenu/services/mainmenu.ts +++ b/src/core/features/mainmenu/services/mainmenu.ts @@ -17,7 +17,6 @@ import { Injectable } from '@angular/core'; import { CoreApp } from '@services/app'; import { CoreLang } from '@services/lang'; import { CoreSites } from '@services/sites'; -import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from './mainmenu-delegate'; import { makeSingleton } from '@singletons'; @@ -42,20 +41,10 @@ export class CoreMainMenuProvider { * * @return Promise resolved with the current main menu handlers. */ - getCurrentMainMenuHandlers(): Promise { - const deferred = CoreUtils.instance.promiseDefer(); - - const subscription = CoreMainMenuDelegate.instance.getHandlersObservable().subscribe((handlers) => { - subscription?.unsubscribe(); - - // Remove the handlers that should only appear in the More menu. - handlers = handlers.filter(handler => !handler.onlyInMore); - - // Return main handlers. - deferred.resolve(handlers.slice(0, this.getNumItems())); - }); - - return deferred.promise; + getCurrentMainMenuHandlers(): CoreMainMenuHandlerToDisplay[] { + return CoreMainMenuDelegate.instance.getHandlers() + .filter(handler => !handler.onlyInMore) + .slice(0, this.getNumItems()); } /** diff --git a/src/core/features/sitehome/services/handlers/index-link.ts b/src/core/features/sitehome/services/handlers/index-link.ts index 567a0230a..505fa2716 100644 --- a/src/core/features/sitehome/services/handlers/index-link.ts +++ b/src/core/features/sitehome/services/handlers/index-link.ts @@ -19,7 +19,7 @@ import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; import { CoreSiteHome } from '../sitehome'; import { makeSingleton } from '@singletons'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Handler to treat links to site home index. @@ -43,7 +43,8 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler getActions(): CoreContentLinksAction[] | Promise { return [{ action: (siteId: string): void => { - CoreNavHelper.instance.goInSite('sitehome', [], siteId); + // @todo This should open the 'sitehome' setting as well. + CoreNavigator.instance.navigateToSiteHome({ siteId }); }, }]; } diff --git a/src/core/features/tag/services/handlers/index.link.ts b/src/core/features/tag/services/handlers/index.link.ts index b4836cd73..0e2103d61 100644 --- a/src/core/features/tag/services/handlers/index.link.ts +++ b/src/core/features/tag/services/handlers/index.link.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { CoreTag } from '../tag'; @@ -57,11 +57,11 @@ export class CoreTagIndexLinkHandlerService extends CoreContentLinksHandlerBase }; if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) { - CoreNavHelper.instance.goInSite('/tag/search', {}, siteId); + CoreNavigator.instance.navigateToSitePath('/tag/search', { siteId }); } else if (pageParams.areaId) { - CoreNavHelper.instance.goInSite('/tag/index-area', pageParams, siteId); + CoreNavigator.instance.navigateToSitePath('/tag/index-area', { params: pageParams, siteId }); } else { - CoreNavHelper.instance.goInSite('/tag/index', pageParams, siteId); + CoreNavigator.instance.navigateToSitePath('/tag/index', { params: pageParams, siteId }); } }, }]; diff --git a/src/core/features/tag/services/handlers/search.link.ts b/src/core/features/tag/services/handlers/search.link.ts index 382a09688..cb025d449 100644 --- a/src/core/features/tag/services/handlers/search.link.ts +++ b/src/core/features/tag/services/handlers/search.link.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { CoreTag } from '../tag'; @@ -47,7 +47,7 @@ export class CoreTagSearchLinkHandlerService extends CoreContentLinksHandlerBase query: params.query || '', }; - CoreNavHelper.instance.goInSite('/tag/search', pageParams, siteId); + CoreNavigator.instance.navigateToSitePath('/tag/search', { params: pageParams, siteId }); }, }]; } diff --git a/src/core/features/user/services/handlers/profile-link.ts b/src/core/features/user/services/handlers/profile-link.ts index ca3f70c6f..d9a98b174 100644 --- a/src/core/features/user/services/handlers/profile-link.ts +++ b/src/core/features/user/services/handlers/profile-link.ts @@ -17,7 +17,7 @@ import { Params } from '@angular/router'; import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate'; -import { CoreNavHelper } from '@services/nav-helper'; +import { CoreNavigator } from '@services/navigator'; import { makeSingleton } from '@singletons'; /** @@ -54,7 +54,7 @@ export class CoreUserProfileLinkHandlerService extends CoreContentLinksHandlerBa userId: parseInt(params.id, 10), }; - CoreNavHelper.instance.goInSite('/user', pageParams, siteId); + CoreNavigator.instance.navigateToSitePath('/user', { params: pageParams, siteId }); }, }]; } diff --git a/src/core/services/app.ts b/src/core/services/app.ts index d14d121a8..e09f53260 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -25,7 +25,6 @@ import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@ import { CoreLogger } from '@singletons/logger'; import { CoreColors } from '@singletons/colors'; import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; -import { CoreNavHelper } from './nav-helper'; /** * Object responsible of managing schema versions. @@ -178,10 +177,10 @@ export class CoreAppProvider { * Get an ID for a main menu. * * @return Main menu ID. - * @deprecated since 3.9.5. Use CoreNavHelperService.getMainMenuId instead. + * @deprecated since 3.9.5. No longer supported. */ getMainMenuId(): number { - return CoreNavHelper.instance.getMainMenuId(); + return 0; } /** @@ -306,10 +305,10 @@ export class CoreAppProvider { * Check if the main menu is open. * * @return Whether the main menu is open. - * @deprecated since 3.9.5. Use CoreNavHelperService.isMainMenuOpen instead. + * @deprecated since 3.9.5. No longer supported. */ isMainMenuOpen(): boolean { - return CoreNavHelper.instance.isMainMenuOpen(); + return false; } /** @@ -445,17 +444,6 @@ export class CoreAppProvider { this.keyboardClosing = false; } - /** - * Set a main menu as open or not. - * - * @param id Main menu ID. - * @param open Whether it's open or not. - * @deprecated since 3.9.5. Use CoreNavHelperService.setMainMenuOpen instead. - */ - setMainMenuOpen(id: number, open: boolean): void { - CoreNavHelper.instance.setMainMenuOpen(id, open); - } - /** * Start an SSO authentication process. * Please notice that this function should be called when the app receives the new token from the browser, diff --git a/src/core/services/nav-helper.ts b/src/core/services/nav-helper.ts deleted file mode 100644 index 69819bac4..000000000 --- a/src/core/services/nav-helper.ts +++ /dev/null @@ -1,364 +0,0 @@ -// (C) Copyright 2015 Moodle Pty Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Injectable } from '@angular/core'; -import { Params, Router } from '@angular/router'; -import { CoreMainMenu } from '@features/mainmenu/services/mainmenu'; -import { NavController } from '@ionic/angular'; -import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; - -import { makeSingleton } from '@singletons'; -import { CoreEvents } from '@singletons/events'; -import { CoreConstants } from '../constants'; -import { CoreSites } from './sites'; -import { CoreDomUtils } from './utils/dom'; -import { CoreTextUtils } from './utils/text'; -import { CoreUrlUtils } from './utils/url'; - -/** - * Provider to provide some helper functions regarding navigation. - */ -@Injectable({ providedIn: 'root' }) -export class CoreNavHelperService { - - static readonly OPEN_COURSE = 'open_course'; - - protected pageToLoad?: {page: string; params?: Params; time: number}; // Page to load once main menu is opened. - protected mainMenuOpen?: number; - protected mainMenuId = 0; - - constructor( - protected router: Router, - protected navCtrl: NavController, - ) { - CoreEvents.on(CoreEvents.MAIN_MENU_OPEN, () => { - /* If there is any page pending to be opened, do it now. Don't open pages stored more than 5 seconds ago, probably - the function to open the page was called when it shouldn't. */ - if (this.pageToLoad && Date.now() - this.pageToLoad.time < 5000) { - this.loadPageInMainMenu(this.pageToLoad.page, this.pageToLoad.params); - delete this.pageToLoad; - } - }); - } - - /** - * Get current page route without params. - * - * @return Current page route. - */ - getCurrentPage(): string { - return CoreUrlUtils.instance.removeUrlParams(this.router.url); - } - - /** - * Open a new page in the current main menu tab. - * - * @param page Page to open. - * @param pageParams Params to send to the page. - * @return Promise resolved when done. - */ - async goInCurrentMainMenuTab(page: string, pageParams: Params): Promise { - const currentPage = this.getCurrentPage(); - - const routeMatch = currentPage.match(/^\/main\/([^/]+)/); - if (!routeMatch || !routeMatch[0]) { - // Not in a tab. Stop. - return; - } - - let path = ''; - if (routeMatch[1] && page.match(new RegExp(`^/${routeMatch[1]}(/|$)`))) { - path = CoreTextUtils.instance.concatenatePaths('/main', page); - } else { - path = CoreTextUtils.instance.concatenatePaths(routeMatch[0], page); - } - - await this.navCtrl.navigateForward(path, { - queryParams: pageParams, - }); - } - - /** - * Goes to a certain page in a certain site. If the site is current site it will perform a regular navigation, - * otherwise it will load the other site and open the page in main menu. - * - * @param path Path to go. - * @param pageParams Params to send to the page. - * @param siteId Site ID. If not defined, current site. - * @param checkMenu If true, check if the root page is on a main menu tab. Only the path will be checked. - * @return Promise resolved when done. - */ - async goInSite(path: string, pageParams: Params, siteId?: string, checkMenu?: boolean): Promise { - - siteId = siteId || CoreSites.instance.getCurrentSiteId(); - - // @todo: When this function was in ContentLinksHelper, this code was inside NgZone. Check if it's needed. - - if (!CoreSites.instance.isLoggedIn() || siteId != CoreSites.instance.getCurrentSiteId()) { - await this.openInSiteMainMenu(path, pageParams, siteId); - - return; - } - - if (checkMenu) { - let isInMenu = false; - // Check if the page is in the main menu. - try { - isInMenu = await CoreMainMenu.instance.isCurrentMainMenuHandler(path); - } catch { - isInMenu = false; - } - - if (isInMenu) { - // Just select the tab. @todo test. - CoreNavHelper.instance.loadPageInMainMenu(path, pageParams); - - return; - } - } - - await this.goInCurrentMainMenuTab(path, pageParams); - } - - /** - * Get an ID for a main menu. - * - * @return Main menu ID. - */ - getMainMenuId(): number { - return this.mainMenuId++; - } - - /** - * Open a page that doesn't belong to any site. - * - * @param page Page to open. - * @param params Params of the page. - * @return Promise resolved when done. - */ - async goToNoSitePage(page: string, params?: Params): Promise { - const currentPage = this.getCurrentPage(); - - if (currentPage == page) { - // Already at page, nothing to do. - return; - } - - if (page == '/login/sites') { - // Just open the page as root. - await this.navCtrl.navigateRoot(page, { queryParams: params }); - - return; - } - - if (page == '/login/credentials' && currentPage == '/login/site') { - // Just open the new page to keep the navigation history. - await this.navCtrl.navigateForward(page, { queryParams: params }); - - return; - } - - // Check if there is any site stored. - const hasSites = await CoreSites.instance.hasSites(); - - if (!hasSites) { - // There are sites stored, open sites page first to be able to go back. - await this.navCtrl.navigateRoot('/login/sites'); - - await this.navCtrl.navigateForward(page, { queryParams: params }); - - return; - } - - if (page != '/login/site') { - // Open the new site page to be able to go back. - await this.navCtrl.navigateRoot('/login/site'); - - await this.navCtrl.navigateForward(page, { queryParams: params }); - } else { - // Just open the page as root. - await this.navCtrl.navigateRoot(page, { queryParams: params }); - } - } - - /** - * Go to the initial page of a site depending on 'userhomepage' setting. - * - * @param options Options. - * @return Promise resolved when done. - */ - goToSiteInitialPage(options?: CoreNavHelperOpenMainMenuOptions): Promise { - return this.openMainMenu(options); - } - - /** - * Check if the main menu is open. - * - * @return Whether the main menu is open. - */ - isMainMenuOpen(): boolean { - return typeof this.mainMenuOpen != 'undefined'; - } - - /** - * Load a certain page in the main menu. - * - * @param page Route of the page to load. - * @param params Params to pass to the page. - */ - loadPageInMainMenu(page: string, params?: Params): void { - if (!this.isMainMenuOpen()) { - // Main menu not open. Store the page to be loaded later. - this.pageToLoad = { - page: page, - params: params, - time: Date.now(), - }; - - return; - } - - if (page == CoreNavHelperService.OPEN_COURSE) { - // @todo Use the openCourse function. - } else { - CoreEvents.trigger(CoreEvents.LOAD_PAGE_MAIN_MENU, { redirectPage: page, redirectParams: params }); - } - } - - /** - * Load a site and load a certain page in that site. - * - * @param siteId Site to load. - * @param page Name of the page to load. - * @param params Params to pass to the page. - * @return Promise resolved when done. - */ - protected async loadSiteAndPage(siteId: string, page: string, params?: Params): Promise { - if (siteId == CoreConstants.NO_SITE_ID) { - // Page doesn't belong to a site, just load the page. - await this.navCtrl.navigateRoot(page, params); - - return; - } - - const modal = await CoreDomUtils.instance.showModalLoading(); - - try { - const loggedIn = await CoreSites.instance.loadSite(siteId, page, params); - - if (!loggedIn) { - return; - } - - await this.openMainMenu({ - redirectPage: page, - redirectParams: params, - }); - } catch (error) { - // Site doesn't exist. - await this.navCtrl.navigateRoot('/login/sites'); - } finally { - modal.dismiss(); - } - } - - /** - * Open the main menu, loading a certain page. - * - * @param options Options. - * @return Promise resolved when done. - */ - protected async openMainMenu(options?: CoreNavHelperOpenMainMenuOptions): Promise { - - // Due to DeepLinker, we need to remove the path from the URL before going to main menu. - // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL. - // @todo this.location.replaceState(''); - - if (options?.redirectPage == CoreNavHelperService.OPEN_COURSE) { - // Load the main menu first, and then open the course. - try { - await this.navCtrl.navigateRoot('/'); - } finally { - // @todo: Open course. - } - } else { - // Open the main menu. - const queryParams: Params = Object.assign({}, options); - delete queryParams.navigationOptions; - - await this.navCtrl.navigateRoot('/', { - queryParams, - ...options?.navigationOptions, - }); - } - } - - /** - * Open a new page, setting it as the root page and loading the right site if needed. - * - * @param page Name of the page to load. Special cases: OPEN_COURSE (to open course page). - * @param params Params to pass to the page. - * @param siteId Site to load. If not defined, current site. - * @return Promise resolved when done. - */ - async openInSiteMainMenu(page: string, params?: Params, siteId?: string): Promise { - siteId = siteId || CoreSites.instance.getCurrentSiteId(); - - if (!CoreSites.instance.isLoggedIn()) { - if (siteId) { - await this.loadSiteAndPage(siteId, page, params); - } else { - await this.navCtrl.navigateRoot('/login/sites'); - } - - return; - } - - if (siteId && siteId != CoreSites.instance.getCurrentSiteId()) { - // Target page belongs to a different site. Change site. - // @todo: Check site plugins. - await CoreSites.instance.logout(); - - await this.loadSiteAndPage(siteId, page, params); - } else { - // Current page, open it in main menu. - this.loadPageInMainMenu(page, params); - } - } - - /** - * Set a main menu as open or not. - * - * @param id Main menu ID. - * @param open Whether it's open or not. - */ - setMainMenuOpen(id: number, open: boolean): void { - if (open) { - this.mainMenuOpen = id; - CoreEvents.trigger(CoreEvents.MAIN_MENU_OPEN); - } else if (this.mainMenuOpen == id) { - delete this.mainMenuOpen; - } - } - -} - -export class CoreNavHelper extends makeSingleton(CoreNavHelperService) {} - -export type CoreNavHelperOpenMainMenuOptions = { - redirectPage?: string; // Route of the page to open in main menu. If not defined, default tab will be selected. - redirectParams?: Params; // Params to pass to the selected tab if any. - urlToOpen?: string; // URL to open once the main menu is loaded. - navigationOptions?: NavigationOptions; // Navigation options. -}; diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts new file mode 100644 index 000000000..7c03690fc --- /dev/null +++ b/src/core/services/navigator.ts @@ -0,0 +1,247 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { NavigationOptions } from '@ionic/angular/providers/nav-controller'; + +import { CoreConstants } from '@/core/constants'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreMainMenu } from '@features/mainmenu/services/mainmenu'; +import { CoreMainMenuHomeHandlerService } from '@features/mainmenu/services/handlers/mainmenu'; +import { CoreObject } from '@singletons/object'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreUrlUtils } from '@services/utils/url'; +import { makeSingleton, NavController, Router } from '@singletons'; + +const DEFAULT_MAIN_MENU_TAB = CoreMainMenuHomeHandlerService.PAGE_NAME; + +/** + * Redirect payload. + */ +export type CoreRedirectPayload = { + redirectPath: string; + redirectParams?: Params; +}; + +/** + * Navigation options. + */ +export type CoreNavigationOptions = { + animated?: boolean; + params?: Params; + reset?: boolean; +}; + +/** + * Service to provide some helper functions regarding navigation. + */ +@Injectable({ providedIn: 'root' }) +export class CoreNavigatorService { + + /** + * Check whether the active route is using the given path. + * + * @param path Path. + * @return Whether the active route is using the given path. + */ + isCurrent(path: string): boolean { + return this.getCurrentPath() === path; + } + + /** + * Get current main menu tab. + * + * @return Current main menu tab or null if the current route is not using the main menu. + */ + getCurrentMainMenuTab(): string | null { + const currentPath = this.getCurrentPath(); + const matches = /^\/main\/([^/]+).*$/.exec(currentPath); + + return matches?.[1] ?? null; + } + + /** + * Navigate to a new path. + * + * @param path Path to navigate to. + * @param options Navigation options. + * @return Whether navigation suceeded. + */ + async navigate(path: string, options: CoreNavigationOptions = {}): Promise { + const url: string[] = [/^[./]/.test(path) ? path : `./${path}`]; + const navigationOptions: NavigationOptions = CoreObject.withoutEmpty({ + animated: options.animated, + queryParams: CoreObject.isEmpty(options.params ?? {}) ? null : options.params, + relativeTo: path.startsWith('/') ? null : this.getCurrentRoute(), + }); + const navigationResult = (options.reset ?? false) + ? await NavController.instance.navigateRoot(url, navigationOptions) + : await NavController.instance.navigateForward(url, navigationOptions); + + return navigationResult !== false; + } + + /** + * Navigate to the login credentials route. + * + * @param params Page params. + * @return Whether navigation suceeded. + */ + async navigateToLoginCredentials(params: Params = {}): Promise { + // If necessary, open the previous path to keep the navigation history. + if (!this.isCurrent('/login/site') && !this.isCurrent('/login/sites')) { + const hasSites = await CoreSites.instance.hasSites(); + + await this.navigate(hasSites ? '/login/sites' : '/login/site', { reset: true }); + } + + // Navigate to login credentials page. + return this.navigate('/login/credentials', { params }); + } + + /** + * Navigate to the home route of the current site. + * + * @param options Navigation options. + * @return Whether navigation suceeded. + */ + async navigateToSiteHome(options: Omit & { siteId?: string } = {}): Promise { + return this.navigateToSitePath(DEFAULT_MAIN_MENU_TAB, options); + } + + /** + * Navigate to a site path, loading the site if necessary. + * + * @param path Site path to visit. + * @param options Navigation and site options. + * @return Whether navigation suceeded. + */ + async navigateToSitePath( + path: string, + options: Omit & { siteId?: string } = {}, + ): Promise { + const siteId = options.siteId ?? CoreSites.instance.getCurrentSiteId(); + const navigationOptions: CoreNavigationOptions = CoreObject.without(options, ['siteId']); + + // @todo: When this function was in ContentLinksHelper, this code was inside NgZone. Check if it's needed. + + // If the path doesn't belong to a site, call standard navigation. + if (siteId === CoreConstants.NO_SITE_ID) { + return this.navigate(path, { + ...navigationOptions, + reset: true, + }); + } + + // If we are logged into a different site, log out first. + if (CoreSites.instance.isLoggedIn() && CoreSites.instance.getCurrentSiteId() !== siteId) { + // @todo: Check site plugins and store redirect. + + await CoreSites.instance.logout(); + } + + // If we are not logged into the site, load the site. + if (!CoreSites.instance.isLoggedIn()) { + const modal = await CoreDomUtils.instance.showModalLoading(); + + try { + const loggedIn = await CoreSites.instance.loadSite(siteId, path, options.params); + + if (!loggedIn) { + // User has been redirected to the login page and will be redirected to the site path after login. + return true; + } + } catch (error) { + // Site doesn't exist. + return this.navigate('/login/sites', { reset: true }); + } finally { + modal.dismiss(); + } + } + + // User is logged in, navigate to the site path. + return this.navigateToMainMenuPath(path, navigationOptions); + } + + /** + * Get the active route path. + * + * @return Current path. + */ + protected getCurrentPath(): string { + return CoreUrlUtils.instance.removeUrlParams(Router.instance.url); + } + + /** + * Get current activated route. + * + * @param route Parent route. + * @return Current activated route. + */ + protected getCurrentRoute(route?: ActivatedRoute): ActivatedRoute { + route = route ?? Router.instance.routerState.root; + + return route.children.length === 0 ? route : this.getCurrentRoute(route.children[0]); + } + + /** + * Navigate to a path within the main menu. + * If the path belongs to a visible tab, that tab will be selected. + * If it doesn't, the current tab or the default tab will be used instead. + * + * @param options Navigation options. + * @return Whether navigation suceeded. + */ + protected async navigateToMainMenuPath(path: string, options: Omit = {}): Promise { + // Due to DeepLinker, we need to remove the path from the URL before going to main menu. + // IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL. + // @todo this.location.replaceState(''); + + path = path.replace(/^(\.|\/main)?\//, ''); + + // Open the path within the corresponding main tab. + const pathRoot = /^[^/]+/.exec(path)?.[0] ?? ''; + const isCurrentMainMenuHandler = await CoreUtils.instance.ignoreErrors( + CoreMainMenu.instance.isCurrentMainMenuHandler(pathRoot), + false, + ); + + if (isCurrentMainMenuHandler) { + return this.navigate(`/main/${path}`, options); + } + + // Open the path within the current main tab. + const currentMainMenuTab = this.getCurrentMainMenuTab(); + + if (currentMainMenuTab) { + return this.navigate(`/main/${currentMainMenuTab}/${path}`, options); + } + + // Open the path within the default main tab. + // @todo test that this is working as expected + return this.navigate(`/main/${DEFAULT_MAIN_MENU_TAB}`, { + ...options, + params: { + redirectPath: `/main/${DEFAULT_MAIN_MENU_TAB}/${path}`, + redirectParams: options.params, + } as CoreRedirectPayload, + }); + } + +} + +export class CoreNavigator extends makeSingleton(CoreNavigatorService) {} diff --git a/src/core/services/tests/navigator.test.ts b/src/core/services/tests/navigator.test.ts new file mode 100644 index 000000000..0d80c2270 --- /dev/null +++ b/src/core/services/tests/navigator.test.ts @@ -0,0 +1,154 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NavController as NavControllerService } from '@ionic/angular'; + +import { mock, mockSingleton } from '@/testing/utils'; + +import { CoreNavigatorService } from '@services/navigator'; +import { CoreUtils, CoreUtilsProvider } from '@services/utils/utils'; +import { CoreUrlUtils, CoreUrlUtilsProvider } from '@services/utils/url'; +import { NavController, Router } from '@singletons'; +import { ActivatedRoute, RouterState } from '@angular/router'; +import { CoreSites } from '@services/sites'; +import { CoreMainMenu } from '@features/mainmenu/services/mainmenu'; + +describe('CoreNavigator', () => { + + let router: { + url?: string; + routerState?: Partial; + }; + let currentMainMenuHandlers: string[]; + let navigator: CoreNavigatorService; + let navControllerMock: NavControllerService; + + beforeEach(() => { + router = { url: '/' }; + currentMainMenuHandlers = ['home']; + navigator = new CoreNavigatorService(); + navControllerMock = mockSingleton(NavController, ['navigateRoot', 'navigateForward']); + + mockSingleton(Router, router); + mockSingleton(CoreUtils, new CoreUtilsProvider(mock())); + mockSingleton(CoreUrlUtils, new CoreUrlUtilsProvider()); + mockSingleton(CoreSites, { getCurrentSiteId: () => 42, isLoggedIn: () => true }); + mockSingleton(CoreMainMenu, { isCurrentMainMenuHandler: path => Promise.resolve(currentMainMenuHandlers.includes(path)) }); + }); + + it('matches against current path', () => { + router.url = '/main/foo'; + + expect(navigator.isCurrent('/main/foo')).toBe(true); + expect(navigator.isCurrent('/main')).toBe(false); + }); + + it('gets the current main menu tab', () => { + expect(navigator.getCurrentMainMenuTab()).toBeNull(); + + router.url = '/main/foo'; + expect(navigator.getCurrentMainMenuTab()).toBe('foo'); + + router.url = '/main/foo/bar'; + expect(navigator.getCurrentMainMenuTab()).toBe('foo'); + }); + + it('navigates to absolute paths', async () => { + const success = await navigator.navigate('/main/foo/bar', { reset: true }); + + expect(success).toBe(true); + expect(navControllerMock.navigateRoot).toHaveBeenCalledWith(['/main/foo/bar'], {}); + }); + + it('navigates to relative paths', async () => { + // Arrange. + const mainOutletRoute = { routeConfig: { path: 'foo' }, children: [] }; + const primaryOutletRoute = { routeConfig: { path: 'main' }, children: [mainOutletRoute] }; + const rootRoute = { children: [primaryOutletRoute] }; + + router.routerState = { root: rootRoute as unknown as ActivatedRoute }; + + // Act. + const success = await navigator.navigate('./bar'); + + // Assert. + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith(['./bar'], { relativeTo: mainOutletRoute }); + }); + + it('navigates to site paths', async () => { + // Arrange + router.url = '/main/foo'; + + // Act + const success = await navigator.navigateToSitePath('/user/42'); + + // Assert + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith(['/main/foo/user/42'], {}); + }); + + it('navigates to site paths using tabs', async () => { + // Arrange + currentMainMenuHandlers.push('users'); + + // Act + const success = await navigator.navigateToSitePath('/users/user/42'); + + // Assert + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith(['/main/users/user/42'], {}); + }); + + it('navigates to site paths using the default tab', async () => { + const success = await navigator.navigateToSitePath('/user/42'); + + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith(['/main/home'], { + queryParams: { + redirectPath: '/main/home/user/42', + }, + }); + }); + + it('navigates to site paths ussing different path formats', async () => { + currentMainMenuHandlers.push('users'); + + const assertNavigation = async (currentPath, sitePath, expectedPath) => { + router.url = currentPath; + + const success = await navigator.navigateToSitePath(sitePath); + + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith([expectedPath], {}); + }; + + await assertNavigation('/main/users', '/main/users/user/42', '/main/users/user/42'); + await assertNavigation('/main/users', '/users/user/42', '/main/users/user/42'); + await assertNavigation('/main/users', '/user/42', '/main/users/user/42'); + await assertNavigation('/main/home', '/users/user/42', '/main/users/user/42'); + }); + + it('navigates to site home', async () => { + const success = await navigator.navigateToSiteHome(); + + expect(success).toBe(true); + expect(navControllerMock.navigateForward).toHaveBeenCalledWith(['/main/home'], {}); + }); + + it.todo('navigates to a different site'); + it.todo('navigates to login credentials'); + it.todo('navigates to NO_SITE_ID site'); + +}); diff --git a/src/core/services/utils/utils.ts b/src/core/services/utils/utils.ts index ba4861dad..16342150d 100644 --- a/src/core/services/utils/utils.ts +++ b/src/core/services/utils/utils.ts @@ -1580,15 +1580,19 @@ export class CoreUtilsProvider { * Ignore errors from a promise. * * @param promise Promise to ignore errors. - * @return Promise with ignored errors. + * @param fallbackResult Value to return if the promise is rejected. + * @return Promise with ignored errors, resolving to the fallback result if provided. */ - async ignoreErrors(promise: Promise): Promise { + async ignoreErrors(promise: Promise): Promise; + async ignoreErrors(promise: Promise, fallback: Fallback): Promise; + async ignoreErrors(promise: Promise, fallback?: Fallback): Promise { try { const result = await promise; return result; } catch (error) { // Ignore errors. + return fallback; } } diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts index 122d0426a..745e3d058 100644 --- a/src/core/singletons/events.ts +++ b/src/core/singletons/events.ts @@ -61,9 +61,7 @@ export class CoreEvents { static readonly KEYBOARD_CHANGE = 'keyboard_change'; static readonly CORE_LOADING_CHANGED = 'core_loading_changed'; static readonly ORIENTATION_CHANGE = 'orientation_change'; - static readonly LOAD_PAGE_MAIN_MENU = 'load_page_main_menu'; static readonly SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; - static readonly MAIN_MENU_OPEN = 'main_menu_open'; static readonly SELECT_COURSE_TAB = 'select_course_tab'; static readonly WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; static readonly SITE_STORAGE_DELETED = 'site_storage_deleted'; @@ -227,14 +225,6 @@ export type CoreEventLoadingChangedData = { uniqueId: string; }; -/** - * Data passed to LOAD_PAGE_MAIN_MENU event. - */ -export type CoreEventLoadPageMainMenuData = { - redirectPage: string; - redirectParams?: Params; -}; - /** * Data passed to COURSE_STATUS_CHANGED event. */ diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index c9a96adce..3353eab15 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -13,6 +13,7 @@ // limitations under the License. import { ApplicationRef, ApplicationInitStatus, Injector, NgZone as NgZoneService, Type } from '@angular/core'; +import { Router as RouterService } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { @@ -23,6 +24,7 @@ import { ToastController as ToastControllerService, GestureController as GestureControllerService, ActionSheetController as ActionSheetControllerService, + NavController as NavControllerService, } from '@ionic/angular'; import { Badge as BadgeService } from '@ionic-native/badge/ngx'; @@ -150,6 +152,8 @@ export class ToastController extends makeSingleton(ToastControllerService) {} export class GestureController extends makeSingleton(GestureControllerService) {} export class ApplicationInit extends makeSingleton(ApplicationInitStatus) {} export class Application extends makeSingleton(ApplicationRef) {} +export class NavController extends makeSingleton(NavControllerService) {} +export class Router extends makeSingleton(RouterService) {} // Convert external libraries injectables. export class Translate extends makeSingleton(TranslateService) {} diff --git a/src/core/singletons/object.ts b/src/core/singletons/object.ts index ac908aa09..589c8b834 100644 --- a/src/core/singletons/object.ts +++ b/src/core/singletons/object.ts @@ -12,24 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. +export type CoreObjectWithoutEmpty = { + [k in keyof T]: T[k] extends undefined | null ? never : T[k]; +}; + /** * Singleton with helper functions for objects. */ export class CoreObject { /** - * Delete all keys from an object whose value are null or undefined. + * Check whether the given object is empty. * - * @param object Object to modify. + * @param object Object. + * @return Whether the given object is empty. */ - static removeUndefined(object: T): T { - for (const name in object) { - if (object[name] === undefined) { - delete object[name]; - } + static isEmpty(object: Record): boolean { + return Object.keys(object).length === 0; + } + + /** + * Create a new object without the specified keys. + * + * @param obj Object. + * @param keys Keys to remove from the new object. + * @return New object without the specified keys. + */ + static without(obj: T, keys: K[]): Omit { + const newObject: T = { ...obj }; + + for (const key of keys) { + delete newObject[key]; } - return object; + return newObject; + } + + /** + * Create a new object without empty values (null or undefined). + * + * @param obj Objet. + * @return New object without empty values. + */ + static withoutEmpty(obj: T): CoreObjectWithoutEmpty { + const cleanObj = {}; + + for (const [key, value] of Object.entries(obj)) { + if (value === null || value === undefined) { + continue; + } + + cleanObj[key] = value; + } + + return cleanObj as CoreObjectWithoutEmpty; } } diff --git a/src/testing/utils.ts b/src/testing/utils.ts index 8bbfddc4e..ab2425723 100644 --- a/src/testing/utils.ts +++ b/src/testing/utils.ts @@ -47,22 +47,26 @@ export function mock( return instance as T; } -export function mockSingleton(singletonClass: CoreSingletonClass, instance?: Record): void; -export function mockSingleton( +export function mockSingleton(singletonClass: CoreSingletonClass, instance: T): T; +export function mockSingleton(singletonClass: CoreSingletonClass, instance?: Record): T; +export function mockSingleton( singletonClass: CoreSingletonClass, methods: string[], instance?: Record, -): void; -export function mockSingleton( - singletonClass: CoreSingletonClass, +): T; +export function mockSingleton( + singletonClass: CoreSingletonClass, methodsOrInstance: string[] | Record = [], instance: Record = {}, -): void { +): T { instance = Array.isArray(methodsOrInstance) ? instance : methodsOrInstance; const methods = Array.isArray(methodsOrInstance) ? methodsOrInstance : []; + const mockInstance = mock(methods, instance); - singletonClass.setInstance(mock(methods, instance)); + singletonClass.setInstance(mockInstance); + + return mockInstance; } export async function renderComponent(component: Type, config: Partial = {}): Promise> {